diff --git a/.all-contributorsrc b/.all-contributorsrc deleted file mode 100644 index c8a93fc4b..000000000 --- a/.all-contributorsrc +++ /dev/null @@ -1,34 +0,0 @@ -{ - "files": [ - "README.md" - ], - "imageSize": 76, - "commit": false, - "contributors": [ - { - "login": "vonovak", - "name": "Vojtech Novak", - "avatar_url": "https://avatars.githubusercontent.com/u/1566403?v=4", - "profile": "https://react-native-training.eu", - "contributions": [ - "code" - ] - }, - { - "login": "kickbk", - "name": "kickbk", - "avatar_url": "https://avatars.githubusercontent.com/u/31323376?v=4", - "profile": "https://github.com/kickbk", - "contributions": [ - "bug", - "test" - ] - } - ], - "contributorsPerLine": 7, - "projectName": "react-native-bottom-sheet", - "projectOwner": "gorhom", - "repoType": "github", - "repoHost": "https://github.com", - "skipCi": true -} diff --git a/.auto-changelog b/.auto-changelog deleted file mode 100644 index 60218bc7a..000000000 --- a/.auto-changelog +++ /dev/null @@ -1,27 +0,0 @@ -{ - "handlebarsSetup": "./scripts/auto-changelog.js", - "ignoreCommitPattern": "release v", - "startingVersion": "v4.0.0-alpha.0", - "unreleased": false, - "commitLimit": false, - "replaceText": { - "([bB]reaking: )": "", - "([bB]reaking change: )": "", - "(^[fF]eat: )": "", - "(^[fF]eat\\()": "(", - "(^[fF]ix: )": "", - "(^[fF]ix\\()": "(", - "(^[cC]hore: )": "", - "(^[cC]hore\\()": "(", - "(^[dD]ocs: )": "", - "(^[dD]ocs\\()": "(", - "(^[rR]efactor: )": "", - "(^[rR]efactor\\()": "(", - "(^[tT]est: )": "", - "(^[tT]est\\()": "(", - "(^[sS]tyle: )": "", - "(^[sS]tyle\\()": "(", - "(^[pP]erf: )": "", - "(^[pP]erf\\()": "(" - } -} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 65365be68..000000000 --- a/.editorconfig +++ /dev/null @@ -1,15 +0,0 @@ -# EditorConfig helps developers define and maintain consistent -# coding styles between different editors and IDEs -# editorconfig.org - -root = true - -[*] - -indent_style = space -indent_size = 2 - -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 8e2f2a064..000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ - -# generated by bob -lib/ diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 60ab22df3..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - root: true, - extends: ['@react-native-community', 'prettier'], - rules: { - 'no-console': ['error', { allow: ['warn', 'error'] }], - 'prettier/prettier': 'error', - }, -}; diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index f94014c36..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '[v4] | [v2] Issue title' -labels: bug -assignees: '' - ---- - -# Bug - - - -## Environment info - - - -| Library | Version | -| ------------------------------- | ------- | -| @gorhom/bottom-sheet | x.x.x | -| react-native | x.x.x | -| react-native-reanimated | x.x.x | -| react-native-gesture-handler | x.x.x | - -## Steps To Reproduce - - - -1. -2. -3. - -Describe what you expected to happen: - -1. -2. - -## Reproducible sample code - - diff --git a/.github/ISSUE_TEMPLATE/bug_template.yaml b/.github/ISSUE_TEMPLATE/bug_template.yaml new file mode 100644 index 000000000..f35cfe4e1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_template.yaml @@ -0,0 +1,95 @@ +name: Bug Report +description: File a bug report. +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + ⚠️ **Please note that issues that do not follow the template will be closed.** + ## Environment Info + - type: dropdown + id: version + attributes: + label: Version + description: What version of the library are you using? + options: + - v5 + - v4 (deprecated) + - v2 (deprecated) + default: 0 + validations: + required: true + - type: dropdown + id: ra-version + attributes: + label: Reanimated Version + description: What version of React Native Reanimated are you using? + options: + - v3 + - v2 (deprecated) + - v1 (deprecated) + default: 0 + validations: + required: true + - type: dropdown + id: gh-version + attributes: + label: Gesture Handler Version + description: What version of Gesture Handler are you using? + options: + - v2 + - v1 (deprecated) + default: 0 + validations: + required: true + + - type: dropdown + id: platform + attributes: + label: Platforms + description: What platform\s this bug is occurring on? + multiple: true + options: + - iOS + - Android + - Web + validations: + required: true + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Please provide a clear and concise description of what the bug is? Include screenshots or gifs if needed. + placeholder: Tell us what happened? + validations: + required: true + + - type: textarea + id: repo-steps + attributes: + label: Reproduction steps + description: You must provide a clear list of steps and code to reproduce the problem. + placeholder: ex. - drag the bottom sheet... + value: "- " + validations: + required: true + + - type: input + id: snack + attributes: + label: Reproduction sample + description: You must provide a reproduction sample code using **Expo Snack** [issue reproduction template](https://snack.expo.dev/@gorhom/bottom-sheet---issue-reproduction-template) + placeholder: ex. https://snack.expo.dev/@gorhom/bottom-sheet---issue-reproduction-template + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell diff --git a/.github/workflows/auto-close.yml b/.github/workflows/auto-close.yml new file mode 100644 index 000000000..ccfd9dde8 --- /dev/null +++ b/.github/workflows/auto-close.yml @@ -0,0 +1,28 @@ +name: Auto Close Issue Workflow + +on: + issues: + types: + - opened + - reopened + - edited + +env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NUMBER: ${{ github.event.issue.number }} + USER: ${{ github.event.issue.user.login }} + REPO: "gorhom/react-native-bottom-sheet" + +jobs: + autoclose: + if: ${{ !contains(github.event.issue.body, 'snack.expo.dev') && !contains(github.event.issue.body, 'gorhom.dev')}} + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Close Issue + run: gh issue close "$NUMBER" --comment "Hello @$USER :wave:, this issue is being automatically closed and locked because it does not follow the issue template." --repo "$REPO" + - name: Label Issue + run: gh issue edit "$NUMBER" --add-label "invalid" --repo "$REPO" + - name: Lock Issue + run: gh issue lock "$NUMBER" -r "spam" --repo "$REPO" \ No newline at end of file diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml deleted file mode 100644 index b61069a6f..000000000 --- a/.github/workflows/documentation.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: documentation - -on: - pull_request: - branches: - - website - push: - branches: - - website - -jobs: - checks: - if: github.event_name != 'push' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: '14' - - name: Test Build - run: | - if [ -e yarn.lock ]; then - yarn install --frozen-lockfile - elif [ -e package-lock.json ]; then - npm ci - else - npm i - fi - npm run build - deploy: - if: github.event_name != 'pull_request' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: '14' - - uses: webfactory/ssh-agent@v0.5.0 - with: - ssh-private-key: ${{ secrets.GH_PAGES_DEPLOY }} - - name: Release to GitHub Pages - env: - USE_SSH: true - GIT_USER: git - CURRENT_BRANCH: website - run: | - git config --global user.email "gorhom@me.com" - git config --global user.name "gorhom" - if [ -e yarn.lock ]; then - yarn install --frozen-lockfile - elif [ -e package-lock.json ]; then - npm ci - else - npm i - fi - npm run deploy \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index b2a765db1..000000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,16 +0,0 @@ -on: - issues: - types: [opened, edited] - -jobs: - auto_close_issues: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Automatically close issues that don't follow the issue template - uses: lucasbento/auto-close-issues@v1.0.2 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - issue-close-message: "@${issue.user.login}: hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template." # optional property - closed-issues-label: "not-following-issue-template" \ No newline at end of file diff --git a/.github/workflows/publish-yarn.yaml b/.github/workflows/publish-yarn.yaml new file mode 100644 index 000000000..5111acaa4 --- /dev/null +++ b/.github/workflows/publish-yarn.yaml @@ -0,0 +1,21 @@ +name: Publish Package to Github Packages +on: + release: + types: [created] +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16.x' + registry-url: 'https://npm.pkg.github.com' + scope: '@discord' + - run: yarn + - run: yarn publish + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 000000000..81b63f7e2 --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,53 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: + - master + paths: + - 'website/**' + +jobs: + build: + name: Build Docusaurus + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: yarn + + - name: Install dependencies + working-directory: website + run: yarn install --frozen-lockfile + - name: Build website + working-directory: website + run: yarn build + + - name: Upload Build Artifact + uses: actions/upload-pages-artifact@v3 + with: + path: website/build + + deploy: + name: Deploy to GitHub Pages + needs: build + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 27dd6343a..161a542e0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ .expo/ # VSCode -.vscode/ jsconfig.json # Xcode @@ -42,7 +41,7 @@ Pods/ # node.js # -node_modules/ +node_modules npm-debug.log yarn-debug.log yarn-error.log @@ -57,3 +56,24 @@ android/keystores/debug.keystore # generated by bob lib/ + +# Dependencies +docs/node_modules + +# Production +docs//build + +# Generated files +docs/.docusaurus +docs/.cache-loader + +# Misc +docs/.DS_Store +docs/.env.local +docs/.env.development.local +docs/.env.test.local +docs/.env.production.local + +docs/npm-debug.log* +docs/yarn-debug.log* +docs/yarn-error.log* diff --git a/.huskyrc.json b/.huskyrc.json index 5a1b2a8cb..4d077c829 100644 --- a/.huskyrc.json +++ b/.huskyrc.json @@ -1,6 +1,5 @@ { "hooks": { - "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", "pre-commit": "lint-staged" } } diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 26f5c15f8..000000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -.github diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index ab0b8187a..000000000 --- a/.prettierrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "printWidth": 80, - "arrowParens": "avoid", - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false -} diff --git a/.release-it.json b/.release-it.json index 92e3dcde2..b1d044e71 100644 --- a/.release-it.json +++ b/.release-it.json @@ -2,22 +2,18 @@ "git": { "push": true, "tagName": "v${version}", - "commitMessage": "chore: release v${version}", - "changelog": "auto-changelog --stdout --unreleased --template ./templates/changelog-template.hbs" + "commitMessage": "chore: release v${version}" }, "github": { - "release": true, - "releaseNotes": "auto-changelog --stdout --unreleased --template ./templates/release-template.hbs" + "release": true }, "npm": { "publish": false }, "plugins": { "@release-it/conventional-changelog": { - "preset": "angular" + "preset": "angular", + "infile": "CHANGELOG.md" } - }, - "hooks": { - "after:bump": "auto-changelog -p --template ./templates/changelog-template.hbs" } } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..330ae6ce4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "quickfix.biome": "always", + "source.organizeImports.biome": "always" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2975b1c59..8c464e2be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,531 +1,103 @@ -## Changelog -### [v4.4.5](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.4.4...v4.4.5) - -#### Refactoring and Updates +## [5.0.6](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.0.5...v5.0.6) (2024-11-17) -- replace findNodeHandle for getRefNativeTag (#1100)(by @AndreiCalazans) ([`1a8928f`](https://github.com/gorhom/react-native-bottom-sheet/commit/1a8928f51cd2b032a2d2d4252e2edcd76f9e32a6)) -- added onPress prop to backdrop component (#1029)(by @tarikpnr) ([`1f0e93f`](https://github.com/gorhom/react-native-bottom-sheet/commit/1f0e93f51f36d82d063db39fdef05159a2ad6f01)) -#### Chores And Housekeeping - -- updated dependencies ([`657ca33`](https://github.com/gorhom/react-native-bottom-sheet/commit/657ca33f6982548f463e092ee186dbe651b7bdb0)) -- updated changelog script and templates ([`ee6230c`](https://github.com/gorhom/react-native-bottom-sheet/commit/ee6230c7e75c03fec1887fe84bc2a0e01f0b1c62)) - -### [v4.4.4](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.4.3...v4.4.4) - 9 September 2022 - -#### Fixes - -- (web): replace setNativeProps with useState (#1076)(by @RobertSasak) ([`625049f`](https://github.com/gorhom/react-native-bottom-sheet/commit/625049f47b266819b0b8a7d96b32e12e46837b37)) -- check if next and current indices are different before animating to a snap position (#1095)(by @itsramiel) ([`3b75d5d`](https://github.com/gorhom/react-native-bottom-sheet/commit/3b75d5d84e0a02933ef2b01d855d9f6036c756b2)) -- don't react to snap point changes if height is 0 (#855)(by @simon-abbott) ([`29af238`](https://github.com/gorhom/react-native-bottom-sheet/commit/29af238d9eed31f0d9cad39ade8a43cf37ca2e72)) - -#### Chores And Housekeeping - -- remove nanoid and react-native-redash to clean up some build issues (#1046) ([`8fc11fd`](https://github.com/gorhom/react-native-bottom-sheet/commit/8fc11fddc0a15f04f20cdcf17532ff17c8946971)) -- updated example packages (#1064) ([`cebae97`](https://github.com/gorhom/react-native-bottom-sheet/commit/cebae97c56f0b2ff31c247b1fce5cbe8172b6554)) -- updated example styling ([`1e99e8d`](https://github.com/gorhom/react-native-bottom-sheet/commit/1e99e8d2e7b73de42b751d32777f18906881eca8)) - -### [v4.4.3](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.4.0...v4.4.3) - 31 July 2022 - -#### Fixes - -- closed bottom sheet snap point (by @eastroot1590) (#1043, #1035) ([`c7f2ce2`](https://github.com/gorhom/react-native-bottom-sheet/commit/c7f2ce26fdaf525951b70b76cd857e0b63cb4865)) - -#### Chores And Housekeeping - -- export internal hook and type ([`a3ae54d`](https://github.com/gorhom/react-native-bottom-sheet/commit/a3ae54dcf7079e88979057f2e19a7813082e798d)) -- updated is-sponsor-label action ([`5281041`](https://github.com/gorhom/react-native-bottom-sheet/commit/5281041bdad5fb522a964e61e8ff79acea16143e)) -- updated sponsor-label action ([`2583e3b`](https://github.com/gorhom/react-native-bottom-sheet/commit/2583e3b18dcde4e1bc449e43f7c0991d257c67df)) -- updated release script ([`a0b64b7`](https://github.com/gorhom/react-native-bottom-sheet/commit/a0b64b7f3da9c6dc811a068fc839efd653c74c16)) - -### [v4.4.0](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.3.2...v4.4.0) - 9 July 2022 - -#### New Features - -- allow scrollable events (#1019) ([`2be6498`](https://github.com/gorhom/react-native-bottom-sheet/commit/2be6498e3c564bd446a92f80df5de5ba6ce5f533)) - -#### Chores And Housekeeping - -- updated git actions ([`bd0a9de`](https://github.com/gorhom/react-native-bottom-sheet/commit/bd0a9de4af48b7babbf524a1b6fc1e799441b207)) -- export internal hooks ([`603ac94`](https://github.com/gorhom/react-native-bottom-sheet/commit/603ac9420a6958a9dfc54975576ed19f306a89e7)) - -### [v4.3.2](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.3.1...v4.3.2) - 13 June 2022 - -#### Fixes - -- (regression): updated keyboard handling reaction (by @yusufyildirim) (#979) ([`1811239`](https://github.com/gorhom/react-native-bottom-sheet/commit/1811239202f7dac2b55bb42cd1155d092f1c5694)) - -### [v4.3.1](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.3.0...v4.3.1) - 24 May 2022 - -#### Fixes - -- removed flex style from draggable view ([`29152fb`](https://github.com/gorhom/react-native-bottom-sheet/commit/29152fb65672a07ff91249a882f0fc0f3d9b796c)) -- added a fixed position for the container on web ([`ce5115a`](https://github.com/gorhom/react-native-bottom-sheet/commit/ce5115a2abd2ddc7140eb3037274b2c5bb3ff10a)) - -#### Refactoring and Updates - -- allow passing style to the container ([`5e1ed9d`](https://github.com/gorhom/react-native-bottom-sheet/commit/5e1ed9da98913d47b27912f49cf7e12b2393176e)) - -#### Chores And Housekeeping - -- added Expo example (#958) ([`cb58a8a`](https://github.com/gorhom/react-native-bottom-sheet/commit/cb58a8aaf90fcd0f7b497b6d1d05db60c7088fde)) -- fixed dynamic snap point example text color ([`321de77`](https://github.com/gorhom/react-native-bottom-sheet/commit/321de777cb848c85a85ac6107ddc26bef1845566)) - -### [v4.3.0](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.2.2...v4.3.0) - 14 May 2022 - -#### New Features - -- added data to present modal api (#942) ([`8a3d138`](https://github.com/gorhom/react-native-bottom-sheet/commit/8a3d13871a40e08e0c3deb302b60bbb2bcffd9f3)) - -#### Refactoring and Updates - -- expose animateOnMount for modals (#943) ([`df3b180`](https://github.com/gorhom/react-native-bottom-sheet/commit/df3b1803f20bcd6cc106984c6aed6c7a271cbff7)) -- added jest mock file (#941) ([`ce15894`](https://github.com/gorhom/react-native-bottom-sheet/commit/ce15894c221fae77f96261eeb5d389eb209ad3a5)) - -### [v4.2.2](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.2.1...v4.2.2) - 2 May 2022 - -#### Fixes - -- allowed keyboard height to be recalculated when it changes (#931) ([`2f33bbe`](https://github.com/gorhom/react-native-bottom-sheet/commit/2f33bbe8ddee66b959100fbe06c54eaf097138df)) - -### [v4.2.1](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.2.0...v4.2.1) - 24 April 2022 - -#### Fixes - -- updated footer container export name ([`a887141`](https://github.com/gorhom/react-native-bottom-sheet/commit/a88714153a780395337b84efe00e3d410702c1d9)) - -### [v4.2.0](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.1.6...v4.2.0) - 24 April 2022 - -#### New Features - -- allow unsafe usage for useBottomSheetInternal & useBottomSheetModalInternal (#740)(by @jembach) ([`1bf6139`](https://github.com/gorhom/react-native-bottom-sheet/commit/1bf613997cb7a7c8d1fd14f8253701e511a145c7)) - -#### Chores And Housekeeping - -- fixed types import from reanimated ([`831df9c`](https://github.com/gorhom/react-native-bottom-sheet/commit/831df9c9e8f25ead974251efcdc384fa1ca00c2e)) -- fixed types import ([`95cb80d`](https://github.com/gorhom/react-native-bottom-sheet/commit/95cb80d3331efb12a1b22b904ebdc0155ebcd833)) -- exported useBottomSheetModalInternal hook ([`31eb738`](https://github.com/gorhom/react-native-bottom-sheet/commit/31eb73859b46ca325d8960baff9a9ddccb1b89fe)) - -### [v4.1.6](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.1.5...v4.1.6) - 23 April 2022 - -#### Fixes - -- updated BottomSheetBackdrop "falsey" default props (#793)(by @jakobo) ([`7e00dd2`](https://github.com/gorhom/react-native-bottom-sheet/commit/7e00dd2e30808a122d28ca1e37eebe19e450b884)) -- always update container height to avoid races. (#919)(by @elan) ([`3245b23`](https://github.com/gorhom/react-native-bottom-sheet/commit/3245b23653a38da2057f28d02f6d2bf1168864d0)) -- always update handle height to avoid races.(related #919) ([`dbf8945`](https://github.com/gorhom/react-native-bottom-sheet/commit/dbf894591db8c72c4a0a4a5f1c2986f07ed4b1fb)) - -#### Documentation Changes - -- updated the readme file ([`d951b17`](https://github.com/gorhom/react-native-bottom-sheet/commit/d951b17957eb5d2f7f1b40a628ba6d5edd4b5a99)) - -#### Chores And Housekeeping - -- updated react native to 0.68 ([`b4614bd`](https://github.com/gorhom/react-native-bottom-sheet/commit/b4614bdd70a82dc31d9ef148a47533682b67a802)) -- updated reanimated to 2.8 ([`c1e6847`](https://github.com/gorhom/react-native-bottom-sheet/commit/c1e6847048c43fb2b678bedfd94ae57502df9765)) -- added native screens example ([`1cf46c0`](https://github.com/gorhom/react-native-bottom-sheet/commit/1cf46c08c5561c0320c57e1006b24b70c690a34f)) -- updated react native portal library ([`955b774`](https://github.com/gorhom/react-native-bottom-sheet/commit/955b7748932ba5ea81d2406c0acf7b612fecbf0e)) -- updated portal to 1.0.12 ([`0010008`](https://github.com/gorhom/react-native-bottom-sheet/commit/0010008906154f9a545f89d5826ea7af48336610)) -- replaced blacklist with exclusionList (#649)(by @aleppos) ([`e3881b3`](https://github.com/gorhom/react-native-bottom-sheet/commit/e3881b3149c522102b93c5d2ed2a23003ece4ca2)) -- export BottomSheetFooterContainer component ([`4f63b0d`](https://github.com/gorhom/react-native-bottom-sheet/commit/4f63b0d0609160790b420d88478859b91fb8424d)) - -### [v4.1.5](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.1.4...v4.1.5) - 5 December 2021 - -#### Fixes - -- resume animation on interruption (#769) ([`f2a9332`](https://github.com/gorhom/react-native-bottom-sheet/commit/f2a933274c88004357700bf728c1c3d1fde48d20)) - -### [v4.1.4](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.1.3...v4.1.4) - 21 November 2021 - -#### Fixes - -- prevent hiding bottom sheet container on platforms other than Android (#719) ([`3da1a2e`](https://github.com/gorhom/react-native-bottom-sheet/commit/3da1a2e6f33fb886e53606d4bbcd06938d839008)) - -#### Documentation Changes - -- updated readme ([`d951a19`](https://github.com/gorhom/react-native-bottom-sheet/commit/d951a1976f5fd2e7a38bedbabb452a103b9644ea)) - -#### Refactoring and Updates - -- updated modal ref calls to use optional chaining (#725)(by @jcgertig) ([`9ace1c6`](https://github.com/gorhom/react-native-bottom-sheet/commit/9ace1c69f1153af8b598724f184672e3f6a807a5)) - -#### Chores And Housekeeping - -- updated example dependencies ([`9176e35`](https://github.com/gorhom/react-native-bottom-sheet/commit/9176e35dec148a8d3eff8b472ccb495b4992d8e1)) - -### [v4.1.3](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.1.2...v4.1.3) - 18 October 2021 - -#### Fixes - -- prevent unstable mounting for modals (#697) ([`657505a`](https://github.com/gorhom/react-native-bottom-sheet/commit/657505a65b01a1ccd7e2027b12fe1953967aa875)) - -#### Documentation Changes - -- updated logo ([`7c176e0`](https://github.com/gorhom/react-native-bottom-sheet/commit/7c176e08eca0be638b283712c643f0ef281134ae)) - -#### Refactoring and Updates - -- updated modal ref calls to use optional chaining (#699)(by @jcgertig) ([`ea19e3f`](https://github.com/gorhom/react-native-bottom-sheet/commit/ea19e3fa17953854c769ef6d2033d14bcd5a747e)) - -#### Chores And Housekeeping - -- updated @gorhom/portal dependency ([`e777487`](https://github.com/gorhom/react-native-bottom-sheet/commit/e77748712772f2da66ea27ddd655fc5b7d75ab02)) -- updated sponsor link ([`2b624cc`](https://github.com/gorhom/react-native-bottom-sheet/commit/2b624ccfb8d5cb6c03337052e86d4d0d8ab960fa)) -- updated contact list scroll indicator style to black ([`9cc8b17`](https://github.com/gorhom/react-native-bottom-sheet/commit/9cc8b172298fa38c2a5597d3ed77361fd496db25)) - -### [v4.1.2](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.1.1...v4.1.2) - 12 October 2021 - -#### Fixes - -- hide the bottom sheet on closed (#690) ([`9f04d55`](https://github.com/gorhom/react-native-bottom-sheet/commit/9f04d557d202ab8570b1b409332bfdd129e5efa4)) - -### [v4.1.1](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.1.0...v4.1.1) - 3 October 2021 - -#### Refactoring and Updates - -- allow to render component inside default backdrop (#662) ([`5df1a1f`](https://github.com/gorhom/react-native-bottom-sheet/commit/5df1a1f35f4dab867b38818d01c0f865091a2e70)) -- calling dismiss without a key will remove the current modal if any (#676)(by @Shywim) ([`fd4bb8d`](https://github.com/gorhom/react-native-bottom-sheet/commit/fd4bb8df8b4dae879326438697a85c0c9d2ddb24)) - -### [v4.1.0](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.3...v4.1.0) - 26 September 2021 - -#### New Features - -- added handling for keyboard height change (#656)(by @Ferossgp) ([`3c5fc57`](https://github.com/gorhom/react-native-bottom-sheet/commit/3c5fc571e6442bd56712e9f4dbba89bbcd93dda1)) - -#### Fixes - -- updated initial position to screen height (#657) ([`dc56417`](https://github.com/gorhom/react-native-bottom-sheet/commit/dc56417c912b068d0ed2487517ae8f2ad2334b57)) -- remove 'removeListener' as it is now deprecated (#635)(by @brianathere) ([`f03b05b`](https://github.com/gorhom/react-native-bottom-sheet/commit/f03b05bbc39bf62f7d97422e717f2998f2e1fada)) -- revert changes on BottomSheetModal that blocked stack behavour ([`15225ae`](https://github.com/gorhom/react-native-bottom-sheet/commit/15225aef40fb5cb789fb077505edb5d710ab9e91)) -- updated asigning velocity in animate worklet (#650) ([`38b635e`](https://github.com/gorhom/react-native-bottom-sheet/commit/38b635ec03d749cc0b7258ae2972ece722e0bb4a)) - -#### Documentation Changes - -- fix overDragResistanceFactor description (#633) ([`1da46f5`](https://github.com/gorhom/react-native-bottom-sheet/commit/1da46f5ade949aaaaff9d0e472c41059e9aaa969)) - -#### Chores And Housekeeping - -- updated @gorhom/portal dependency ([`366e46b`](https://github.com/gorhom/react-native-bottom-sheet/commit/366e46bc44eb63f8e6bf99d225612c9659b4a72a)) - -### [v4.0.3](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.2...v4.0.3) - 2 September 2021 - -#### Fixes - -- allow content to be accessible #619 ([`f1baf0e`](https://github.com/gorhom/react-native-bottom-sheet/commit/f1baf0e4748fd84110d905f82404a86fd697c936)) - -### [v4.0.2](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.1...v4.0.2) - 31 August 2021 - -#### Fixes - -- updated types for styles (#616) ([`7fa1453`](https://github.com/gorhom/react-native-bottom-sheet/commit/7fa14531fe2fe28ba9385fdcb22e4ca5e6aacf9e)) - -### [v4.0.1](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0...v4.0.1) - 30 August 2021 - -#### Fixes - -- pass correct params to animateToPosition (#610) ([`01883fb`](https://github.com/gorhom/react-native-bottom-sheet/commit/01883fb9575574c228cd40ec4a43658a6ea831c9)) - -#### Documentation Changes - -- add kickbk as a contributor for bug, test (#612) ([`3316c8b`](https://github.com/gorhom/react-native-bottom-sheet/commit/3316c8b92662e5be92d2c355f3fa04632eb8b6bf)) -- add vonovak as a contributor for code (#611) ([`7c97e8f`](https://github.com/gorhom/react-native-bottom-sheet/commit/7c97e8ffd76936a5168ad9f914bdc5e1ab1b3bdd)) - -### [v4.0.0](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.30...v4.0.0) - 30 August 2021 - -#### Documentation Changes - -- added auto-deployment for documentation website ([`3b14281`](https://github.com/gorhom/react-native-bottom-sheet/commit/3b1428199f49339d5aa8a607cd0f496907fcb2e5)) -- updated readme file ([`84fdcf6`](https://github.com/gorhom/react-native-bottom-sheet/commit/84fdcf6db98a5c58ee0b8cfa821bd8031c710df0)) - -#### Chores And Housekeeping - -- updated close method type ([`ca3a11a`](https://github.com/gorhom/react-native-bottom-sheet/commit/ca3a11a3f56f3ba3bcd865ce1006490f3819f054)) - -### [v4.0.0-alpha.30](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.29...v4.0.0-alpha.30) - 22 August 2021 - -#### Fixes - -- prevent the sheet from snapping while layout being calculated ([`445a964`](https://github.com/gorhom/react-native-bottom-sheet/commit/445a9645366af04931f4464d1befb1bc8e1dbbed)) - -#### Refactoring and Updates - -- added forceClose and remove force param from close method ([`3dd5796`](https://github.com/gorhom/react-native-bottom-sheet/commit/3dd5796eb722e4e579de7b2439d224a5e0238b55)) -- clean up animation configs variables #572 ([`8e002e1`](https://github.com/gorhom/react-native-bottom-sheet/commit/8e002e1c20c019951bbf444fceacefc0cf0e86c2)) - -#### Chores And Housekeeping - -- delete debug view from builds ([`7ead04e`](https://github.com/gorhom/react-native-bottom-sheet/commit/7ead04edc1a77cf820adcdadecc912b7791ab14c)) - -### [v4.0.0-alpha.29](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.28...v4.0.0-alpha.29) - 18 August 2021 - -#### New Features - -- added backgroundStyle, handleStyle & handleIndicatorStyle to bottom sheet ([`2211765`](https://github.com/gorhom/react-native-bottom-sheet/commit/221176546fd59ed0c9d79fe7f0350eda24dd8550)) - -#### Fixes - -- prevent keyboard change to snap sheet while user is interacting ([`dd632b0`](https://github.com/gorhom/react-native-bottom-sheet/commit/dd632b04651d37ab6a8a2aba2be13d9633e677e4)) - -### [v4.0.0-alpha.28](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.27...v4.0.0-alpha.28) - 17 August 2021 - -#### Fixes - -- provide dynamic initial snap points while layout is calculating (#584) ([`98fb8d2`](https://github.com/gorhom/react-native-bottom-sheet/commit/98fb8d24a55c064f0072c74c0bf2e1af079be819)) -- prevent snap points lower than 0 ([`95ea72a`](https://github.com/gorhom/react-native-bottom-sheet/commit/95ea72a459f96d40ad583c5579cc72f0e128e5dd)) - -#### Chores And Housekeeping - -- updated github workflow and templates ([`db68fac`](https://github.com/gorhom/react-native-bottom-sheet/commit/db68fac9eb4ac117e7c89dd74352391a77f0a3ec)) -- updated auto-close action version ([`991d214`](https://github.com/gorhom/react-native-bottom-sheet/commit/991d2141a4f026068737abc098f9b0d2b6968a5f)) - -### [v4.0.0-alpha.27](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.26...v4.0.0-alpha.27) - 15 August 2021 - -#### Refactoring and Updates - -- rename Touchables.android to Touchables, to allow web usage ([`a95e34f`](https://github.com/gorhom/react-native-bottom-sheet/commit/a95e34fc2d0af0aaecf514ebbd0e8dee9df55fb0)) - -### [v4.0.0-alpha.26](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.25...v4.0.0-alpha.26) - 15 August 2021 - -#### New Features - -- added onClose callback to BottomSheet ([`ee64545`](https://github.com/gorhom/react-native-bottom-sheet/commit/ee64545ce0e7609fb383f1473773c8481a0bc7aa)) - -#### Fixes - -- updated animated closed position value on detached ([`833879f`](https://github.com/gorhom/react-native-bottom-sheet/commit/833879f3f703b80fb5bc591a823d86f3c56cc7ee)) - -#### Documentation Changes - -- added code of conduct file ([`18a32e5`](https://github.com/gorhom/react-native-bottom-sheet/commit/18a32e5979d22a693734d1af7fef6cc9887cea67)) - -#### Refactoring and Updates - -- updated footer api ([`2cf7289`](https://github.com/gorhom/react-native-bottom-sheet/commit/2cf72890abd92b7e9be25d7013744fe503107a1a)) - -#### Chores And Housekeeping - -- updated package dependencies ([`e11dc84`](https://github.com/gorhom/react-native-bottom-sheet/commit/e11dc844a7cdcba694a01d4cbeb37f1709e23dea)) -- renamed the branch to master ([`a0bb98a`](https://github.com/gorhom/react-native-bottom-sheet/commit/a0bb98a77686687e643514d131b74f421b5d4aee)) - -### [v4.0.0-alpha.25](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.24...v4.0.0-alpha.25) - 6 August 2021 - -#### Fixes - -- fixed the multiline issue on BottomSheetTextInput #411 ([`e21d676`](https://github.com/gorhom/react-native-bottom-sheet/commit/e21d6762a929c6eaaf64e95d8af2934cc8b3a703)) - -### [v4.0.0-alpha.24](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.23...v4.0.0-alpha.24) - 5 August 2021 - -#### Fixes - -- prevent animatedIndex from flickering caused by content resizing ([`7fef5d0`](https://github.com/gorhom/react-native-bottom-sheet/commit/7fef5d03c0edef5945dc0bd825ce9081b90e7402)) - -### [v4.0.0-alpha.23](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.22...v4.0.0-alpha.23) - 5 August 2021 - -#### New Features - -- allow custom pan gesture and scroll handler customisation (#525) (by @vonovak) ([`4c32da7`](https://github.com/gorhom/react-native-bottom-sheet/commit/4c32da7c0bb7e902883f009f10909286ad65042c)) - -#### Fixes - -- allow user to override showsVerticalScrollIndicator value on scrollables ([`11cdc34`](https://github.com/gorhom/react-native-bottom-sheet/commit/11cdc344e029200435280389b291441c1c363e97)) - -#### Refactoring and Updates - -- updated animateOnMount default value to true ([`6293fe4`](https://github.com/gorhom/react-native-bottom-sheet/commit/6293fe452f54c3f5d2ac332642b4c369bc768c92)) - -#### Chores And Housekeeping - -- remove unnecessary useMemos (#515) ([`51fa2b3`](https://github.com/gorhom/react-native-bottom-sheet/commit/51fa2b36989c5ee8a73d3a13a903c49392a4419a)) -- removed enableFlashScrollableIndicatorOnExpand prop ([`e447da4`](https://github.com/gorhom/react-native-bottom-sheet/commit/e447da49a79f09456603cf57b5839c42f390f9b5)) - -### [v4.0.0-alpha.22](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.21...v4.0.0-alpha.22) - 20 July 2021 - -#### Refactoring and Updates - -- allow closing animation to be interrupted ([`937f9ee`](https://github.com/gorhom/react-native-bottom-sheet/commit/937f9ee91c485759c492b9dec532914ffa40375b)) - -### [v4.0.0-alpha.21](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.20...v4.0.0-alpha.21) - 18 July 2021 - -#### New Features - -- react to index prop changes ([`55af54b`](https://github.com/gorhom/react-native-bottom-sheet/commit/55af54bd772ff312f91891d7c88f33afa02f1efe)) - -#### Fixes - -- updated detached bottom sheet handling ([`603f492`](https://github.com/gorhom/react-native-bottom-sheet/commit/603f49294e572716d7eaf517a2adde01681c56c6)) - -### [v4.0.0-alpha.20](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.19...v4.0.0-alpha.20) - 13 July 2021 - -#### Fixes - -- prevent stuck state when animation is interrupted ([`01e1e87`](https://github.com/gorhom/react-native-bottom-sheet/commit/01e1e8716477aa904bedbda2aa08642f8a0c3c9c)) - -#### Refactoring and Updates - -- removed none from keyboard behavior and set interactive as default ([`26d3b71`](https://github.com/gorhom/react-native-bottom-sheet/commit/26d3b7187cb309ce77dd55c32d44a63316776515)) - -### [v4.0.0-alpha.19](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.18...v4.0.0-alpha.19) - 4 July 2021 - -#### Fixes - -- stablise animated index when reacting to keyboard status ([`26132c1`](https://github.com/gorhom/react-native-bottom-sheet/commit/26132c14871af82eda7adf63ea98ab7a9f7d95e3)) - -### [v4.0.0-alpha.18](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.17...v4.0.0-alpha.18) - 1 July 2021 - -#### Fixes - -- fixed handling dynamic snap point on mount snapping ([`35b2fcb`](https://github.com/gorhom/react-native-bottom-sheet/commit/35b2fcb7d4eb1a2b953280a56396459b43b8767e)) - -### [v4.0.0-alpha.17](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.16...v4.0.0-alpha.17) - 29 June 2021 - -#### Fixes - -- updated android keyboard handling ([`f53306d`](https://github.com/gorhom/react-native-bottom-sheet/commit/f53306d8d214d7dc605eb5ecb343f08f011c3ae2)) - -### [v4.0.0-alpha.16](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.15...v4.0.0-alpha.16) - 27 June 2021 - -#### New Features - -- allow view scrollble to over-drag sheet ([`2c2ca4e`](https://github.com/gorhom/react-native-bottom-sheet/commit/2c2ca4ec17587689c2e38fcb0aad87a172251b55)) - -#### Fixes - -- updated keyboard handling for Android ([`2d74ab0`](https://github.com/gorhom/react-native-bottom-sheet/commit/2d74ab069357f0ba430ff9f059dad0c6305eef48)) - -### [v4.0.0-alpha.15](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.14...v4.0.0-alpha.15) - 26 June 2021 - -### [v4.0.0-alpha.14](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.13...v4.0.0-alpha.14) - 26 June 2021 - -#### Fixes - -- refactored snap points reaction to handle keyboard state (#497) ([`f8f2417`](https://github.com/gorhom/react-native-bottom-sheet/commit/f8f2417454480207ae7a5a481b9fcd1483043e23)) - -### [v4.0.0-alpha.13](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.12...v4.0.0-alpha.13) - 15 June 2021 - -#### Fixes - -- prevent animation to same position ([`9636f84`](https://github.com/gorhom/react-native-bottom-sheet/commit/9636f847d53ff99d801753254876722050cc3e13)) - -### [v4.0.0-alpha.12](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.11...v4.0.0-alpha.12) - 12 June 2021 - -#### New Features - -- added detached bottom sheet (#487) ([`3aa5fdb`](https://github.com/gorhom/react-native-bottom-sheet/commit/3aa5fdbce75acf47f534e69b3a898abbf7dfca46)) - -#### Documentation Changes - -- updated detached prop description ([`9d4779b`](https://github.com/gorhom/react-native-bottom-sheet/commit/9d4779b57f60bba7f895f7609e759e0eb0b2640a)) - -#### Chores And Housekeeping - -- updated portal dependency ([`70d72ec`](https://github.com/gorhom/react-native-bottom-sheet/commit/70d72ecff5c78c397dbfc47bbff94b52237efab8)) - -### [v4.0.0-alpha.11](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.10...v4.0.0-alpha.11) - 6 June 2021 - -### [v4.0.0-alpha.10](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.9...v4.0.0-alpha.10) - 6 June 2021 - -#### New Features - -- added pull to refresh implementaion ([`016a01f`](https://github.com/gorhom/react-native-bottom-sheet/commit/016a01f3705c83c9903a3e28c875e7b90424a128)) -- introduced more stable handling for dynamic snap points ([`3edb2d1`](https://github.com/gorhom/react-native-bottom-sheet/commit/3edb2d1f9a9a8b1ba2e04803cd12306e4353199b)) - -#### Fixes - -- dismiss keyboard when sheet position change on Android ([`8f34990`](https://github.com/gorhom/react-native-bottom-sheet/commit/8f34990436f8cc8c1ec1c545488d77db5845166c)) - -### [v4.0.0-alpha.9](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.8...v4.0.0-alpha.9) - 3 June 2021 - -#### New Features - -- added keyboard input mode for android ([`069c4b6`](https://github.com/gorhom/react-native-bottom-sheet/commit/069c4b6742630dc5fa7d4763a5c4dc6bfec439cc)) - -#### Chores And Housekeeping - -- export useBottomSheetInternal, added animatedPosition and animatedIndex to useBottomSheet ([`fb3df59`](https://github.com/gorhom/react-native-bottom-sheet/commit/fb3df595c0bf5bcc63ca29e8e2609929de63e595)) - -### [v4.0.0-alpha.8](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.7...v4.0.0-alpha.8) - 2 June 2021 - -#### Fixes - -- updated typings for sectionlist to mirror rn core types (#475) ([`dd9dbdc`](https://github.com/gorhom/react-native-bottom-sheet/commit/dd9dbdc8d9fbeb5d557cee37841c5ca187c1b5fb)) -- prevent animated content height value from getting below zero ([`d9b417f`](https://github.com/gorhom/react-native-bottom-sheet/commit/d9b417f703ceb69a959b0ce59600e53d75560d1e)) -- updated BottomSheetContainer measuring on android ([`d0e5227`](https://github.com/gorhom/react-native-bottom-sheet/commit/d0e52270076617242010b08f73fe09ab8ede69d1)) - -#### Chores And Housekeeping - -- minor refactor (#473) ([`e209ebe`](https://github.com/gorhom/react-native-bottom-sheet/commit/e209ebe67aabe1d78710a65bda1435387d75dd39)) -- minor simplifications (#467) ([`7cfe70d`](https://github.com/gorhom/react-native-bottom-sheet/commit/7cfe70dda633c3953e7c6bdb3fabcf54408529e8)) - -### [v4.0.0-alpha.7](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.6...v4.0.0-alpha.7) - 30 May 2021 - -#### New Features - -- allow handle to drag sheet without effecting the scrollable ([`580b763`](https://github.com/gorhom/react-native-bottom-sheet/commit/580b7632e656403b0797c4e969a35d30f0ec5cb3)) - -### [v4.0.0-alpha.6](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.5...v4.0.0-alpha.6) - 28 May 2021 - -#### Fixes - -- scrollble container style crash ([`a4b9b93`](https://github.com/gorhom/react-native-bottom-sheet/commit/a4b9b933268a670fbf6dd1198de61d899abde738)) +### Bug Fixes -### [v4.0.0-alpha.5](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.4...v4.0.0-alpha.5) - 27 May 2021 +* clipped views when keyboard is closing ([2320a81](https://github.com/gorhom/react-native-bottom-sheet/commit/2320a81f95e696e22debe5a823740f51fadae0f6)) +* removed keyboard height setting from hide event ([61473b5](https://github.com/gorhom/react-native-bottom-sheet/commit/61473b56c3389e5ac9edfeb1dc4b93907e3b5d05)) +* updated useStableCallback to set callback in ref without useEffect ([#2010](https://github.com/gorhom/react-native-bottom-sheet/issues/2010))(by [@pavel-krasnov](https://github.com/pavel-krasnov)) ([e898859](https://github.com/gorhom/react-native-bottom-sheet/commit/e89885936391f5ce106983d8aac814bcb422e82c)) +* useStableCallback implementation ([87a73c5](https://github.com/gorhom/react-native-bottom-sheet/commit/87a73c59b83ef0b3868c12403a467ea3aebf0dd5)) -#### New Features +## [5.0.5](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.0.4...v5.0.5) (2024-10-26) -- added pre-integrated VirtualizedList component ([`2d4d69d`](https://github.com/gorhom/react-native-bottom-sheet/commit/2d4d69d8881a3cbe452f5e46157e2b9702528206)) -#### Fixes +### Bug Fixes -- updated keyboard height in container calculation ([`2599f6c`](https://github.com/gorhom/react-native-bottom-sheet/commit/2599f6cf46af0f95812e34670de5a7cae5d44fd9)) -- re-snap to current position when snap points get updated ([`bb8e202`](https://github.com/gorhom/react-native-bottom-sheet/commit/bb8e202af05dc6beeb108cfa1680401374ac58ad)) -- handle initial closed sheet ([`4bc40d9`](https://github.com/gorhom/react-native-bottom-sheet/commit/4bc40d93da05dcff664ce939a9944416b9e91359)) +* **#1983:** updated shared values access as hook dependancies ([#1992](https://github.com/gorhom/react-native-bottom-sheet/issues/1992))(by [@pinpong](https://github.com/pinpong)) ([9757bd2](https://github.com/gorhom/react-native-bottom-sheet/commit/9757bd251cba67cf26489640f20fd1557b1a426e)), closes [#1983](https://github.com/gorhom/react-native-bottom-sheet/issues/1983) [#1983](https://github.com/gorhom/react-native-bottom-sheet/issues/1983) +* added BottomSheetFlashList mock ([#1988](https://github.com/gorhom/react-native-bottom-sheet/issues/1988))(by @Fadikk367) ([13c7d47](https://github.com/gorhom/react-native-bottom-sheet/commit/13c7d47beae6f2451968d30e862f0ea49b7199b6)) -### [v4.0.0-alpha.4](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.3...v4.0.0-alpha.4) - 25 May 2021 +## [5.0.4](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.0.3...v5.0.4) (2024-10-20) -#### New Features -- added footer component (#457) ([`46fb883`](https://github.com/gorhom/react-native-bottom-sheet/commit/46fb88398ec7625c258cd62cb8560d72f3537fcb)) +### Bug Fixes -### [v4.0.0-alpha.3](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.2...v4.0.0-alpha.3) - 23 May 2021 +* **#1983:** updated shared values access as hook dependancies ([ae41b2d](https://github.com/gorhom/react-native-bottom-sheet/commit/ae41b2da650d2be614d840fbdfe1d29db6d7a575)), closes [#1983](https://github.com/gorhom/react-native-bottom-sheet/issues/1983) +* **#1987:** updated provided style handling for bottom sheet view ([4c8ae25](https://github.com/gorhom/react-native-bottom-sheet/commit/4c8ae252b8ec0bb420b60f8314cc7f04ed12b519)), closes [#1987](https://github.com/gorhom/react-native-bottom-sheet/issues/1987) -#### Fixes +## [5.0.3](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.0.2...v5.0.3) (2024-10-20) -- on mount flicker on fixed sheet ([`48c4988`](https://github.com/gorhom/react-native-bottom-sheet/commit/48c49888b95dc88abf320d4d7590f43806e0bd59)) -- prevented animatedSnapPoints reaction from running randomly ([`bf4e461`](https://github.com/gorhom/react-native-bottom-sheet/commit/bf4e461e2cb9b5cb90a7de105637fc43d3947525)) -#### Refactoring and Updates +### Bug Fixes -- removed deprecated props (#452) ([`993f936`](https://github.com/gorhom/react-native-bottom-sheet/commit/993f9369dbf62c3e6d193e843e0e2dc7b82dbd50)) +* added children type to containerComponent prop type ([#1971](https://github.com/gorhom/react-native-bottom-sheet/issues/1971))(by @Nodonisko) ([203e52f](https://github.com/gorhom/react-native-bottom-sheet/commit/203e52fa5be3e167522776f184d79511bdf35344)) +* dynamic sizing with detached static views ([b72e275](https://github.com/gorhom/react-native-bottom-sheet/commit/b72e27519c36671d84973f8b0b9cd1f8a7a8b8c1)) +* fixed dynamic scrollables content size with footer in place ([ace0da7](https://github.com/gorhom/react-native-bottom-sheet/commit/ace0da7475d68d4f27d386ead9f71c2eb19fbe31)) +* updated reduce motion handling, to respeact user setting and allow overriding ([1ef05c7](https://github.com/gorhom/react-native-bottom-sheet/commit/1ef05c7fee821c356220452ccf61d33d29483c00)) -### [v4.0.0-alpha.2](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.1...v4.0.0-alpha.2) - 23 May 2021 +## [5.0.2](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.0.1...v5.0.2) (2024-10-14) -#### Refactoring and Updates -- updated handling animated heights (#451) ([`b9313ba`](https://github.com/gorhom/react-native-bottom-sheet/commit/b9313baadc7ea5418be44a7f18bff578be73bac2)) +### Bug Fixes -### [v4.0.0-alpha.1](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.0.0-alpha.0...v4.0.0-alpha.1) - 16 May 2021 +* **#1035,#1043:** updated default animatedNextPositionIndex to INITIAL_VALUE ([#1960](https://github.com/gorhom/react-native-bottom-sheet/issues/1960))(by [@dfalling](https://github.com/dfalling)) ([1cf3e41](https://github.com/gorhom/react-native-bottom-sheet/commit/1cf3e4167f2ffacf36c7abebb527f79048754121)), closes [#1035](https://github.com/gorhom/react-native-bottom-sheet/issues/1035) [#1043](https://github.com/gorhom/react-native-bottom-sheet/issues/1043) +* **#1968:** moved the flashlist optional import into the component body ([ab33e21](https://github.com/gorhom/react-native-bottom-sheet/commit/ab33e2132f8e6fdb4a3c36e34c0f2ff04e09f11f)), closes [#1968](https://github.com/gorhom/react-native-bottom-sheet/issues/1968) -#### New Features +## [5.0.1](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.0.0...v5.0.1) (2024-10-14) -- added snap to position (#443) ([`9ca5f29`](https://github.com/gorhom/react-native-bottom-sheet/commit/9ca5f29b200e1192712859dd9fe31f8c411fadf1)) -### [v4.0.0-alpha.0](https://github.com/gorhom/react-native-bottom-sheet/compare/v3.6.6...v4.0.0-alpha.0) - 16 May 2021 +### Bug Fixes -#### New Features +* removed redundant dependency ([3ffc7f7](https://github.com/gorhom/react-native-bottom-sheet/commit/3ffc7f70e8769fc1ecc39754111754b53d12bff8)) -- added enable pan down to close (#437) ([`1f103b0`](https://github.com/gorhom/react-native-bottom-sheet/commit/1f103b0d2c0a1661213b8c63af1db24cb0c191f7)) +# [5.0.0](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.6.4...v5.0.0) (2024-10-13) -#### Fixes +### Features -- sheet positioning on modals ([`ee573e9`](https://github.com/gorhom/react-native-bottom-sheet/commit/ee573e9463836301d9736c3e5d86b2b363f9fb14)) -- prevent animatedPosition from becoming undefined ([`400d7b9`](https://github.com/gorhom/react-native-bottom-sheet/commit/400d7b93caa0a46f678db2978e7e5f95cc87ee99)) +* added web support (#1150) ([`a996b4a`](https://github.com/gorhom/react-native-bottom-sheet/commit/a996b4aa68139136ec75e0921025d235471c838d)) +* added flashlist as a scrollable ([9bf39ed](https://github.com/gorhom/react-native-bottom-sheet/commit/9bf39ed08d7377937b0e8b8af65791b178c06492)) +* rewrite gesture apis with gesture handler 2 (#1126) ([`6a4d296`](https://github.com/gorhom/react-native-bottom-sheet/commit/6a4d2967684b01e28f23b1b35afbb4cc4dabaf1d)) +* added accessibility overrides support ([#1288](https://github.com/gorhom/react-native-bottom-sheet/issues/1288))(by @Mahmoud-SK) ([6203c18](https://github.com/gorhom/react-native-bottom-sheet/commit/6203c18acc9f8dc3a31af5bf5ad80e368deceb52)) +* added default dynamic sizing ([#1513](https://github.com/gorhom/react-native-bottom-sheet/issues/1513))(with @Eli-Nathan & [@ororsatti](https://github.com/ororsatti)) ([#1683](https://github.com/gorhom/react-native-bottom-sheet/issues/1683)) ([8017fb6](https://github.com/gorhom/react-native-bottom-sheet/commit/8017fb6b02088d3c66c64a8a23e0f63f22884d36)) +* added a new bottom sheet stack behaviour `replace` ([#1897](https://github.com/gorhom/react-native-bottom-sheet/issues/1897))(with [@janodetzel](https://github.com/janodetzel)) ([997d794](https://github.com/gorhom/react-native-bottom-sheet/commit/997d794ccffe8739268ec50dfecca624e10f8752)) -#### Refactoring and Updates +### Bug Fixes -- create one generic scrollable component (#442) ([`01f791e`](https://github.com/gorhom/react-native-bottom-sheet/commit/01f791e42874a5c9bf1b18df029e32c30a51e8b5)) -- converted all internal state/memoized variables to reanimated shared values. (#430) ([`89098e9`](https://github.com/gorhom/react-native-bottom-sheet/commit/89098e9c430917ec0930f6de64b9cb18663242ab)) +* addressed an edge case with scrollview content sizing on initial rendering on safari ([d1226b7](https://github.com/gorhom/react-native-bottom-sheet/commit/d1226b70ac2405b4a98c8e5be6cee94ae110a35b)) +* replaced deprecated reanimated Extrapolate with Extrapolation ([#1875](https://github.com/gorhom/react-native-bottom-sheet/issues/1875))(by [@cenksari](https://github.com/cenksari)) ([5af3e80](https://github.com/gorhom/react-native-bottom-sheet/commit/5af3e803b0313154f42fbadba7dae6d32719c01c)) +* updated animation sequencing to respect force closing by user ([#1941](https://github.com/gorhom/react-native-bottom-sheet/issues/1941)) ([e4f3fe3](https://github.com/gorhom/react-native-bottom-sheet/commit/e4f3fe339b20a28d8573fa31f0d1b85be3ef2085)) +* updated the enable content panning gesture logic ([2962a2d](https://github.com/gorhom/react-native-bottom-sheet/commit/2962a2d5326e517a48fe11d0e0d762beacca890d)) +* updated the scrollable locking logic while scrolling ([#1939](https://github.com/gorhom/react-native-bottom-sheet/issues/1939)) ([d2b959c](https://github.com/gorhom/react-native-bottom-sheet/commit/d2b959c1f25f1aaeed1b30d21c43809c72490ef3)) +* updated the keyboard handling for Android with keyboard input mode resize ([08db4ab](https://github.com/gorhom/react-native-bottom-sheet/commit/08db4ab4b0058955e9ee2d55f87da8fefb5390ad)) +* replace getRefNativeTag with findNodeHandle ([#1823](https://github.com/gorhom/react-native-bottom-sheet/issues/1823))(by @AndreiCalazans) ([866b4ee](https://github.com/gorhom/react-native-bottom-sheet/commit/866b4ee570fc345d59053561c26af67144e8fd6f)) +* **BottomSheetContainer:** cannot add new property 'value' ([#1808](https://github.com/gorhom/react-native-bottom-sheet/issues/1808))(by @MoritzCooks) ([ccd6bb5](https://github.com/gorhom/react-native-bottom-sheet/commit/ccd6bb540884f35fb9c0dcd5527ed8bac0c1be91)) +* added error message when dynamic sizing enabled with a wrong children type ([8b62dca](https://github.com/gorhom/react-native-bottom-sheet/commit/8b62dca06752a3c047162a693a75173a7c701e3e)) +* bottom sheet not appearing for users that have reduced motion turned on ([#1743](https://github.com/gorhom/react-native-bottom-sheet/issues/1743))(by [@fobos531](https://github.com/fobos531)) ([9b4ef4d](https://github.com/gorhom/react-native-bottom-sheet/commit/9b4ef4dabb7ce1f846ae90e2bab39fa9354ff125)) +* fixed the mount animation with reduce motion enabled ([#1560](https://github.com/gorhom/react-native-bottom-sheet/issues/1560), [#1674](https://github.com/gorhom/react-native-bottom-sheet/issues/1674)) ([6efd8ae](https://github.com/gorhom/react-native-bottom-sheet/commit/6efd8aeb0e312555fa77609869eedbf46a4a04b3)) +* added BottomSheetTextInput to the mock file ([#1698](https://github.com/gorhom/react-native-bottom-sheet/issues/1698))(by [@ghorbani-m](https://github.com/ghorbani-m)) ([dee95e5](https://github.com/gorhom/react-native-bottom-sheet/commit/dee95e5b161d78b0aae34d85abea3d8042417892)) +* added footer height to content height when using dynamic sizing ([#1725](https://github.com/gorhom/react-native-bottom-sheet/issues/1725)) ([5009085](https://github.com/gorhom/react-native-bottom-sheet/commit/50090859f9e50932c641df5b0d6f91cc9f3b5bad)) +* added missing mock of Touchables ([#1700](https://github.com/gorhom/react-native-bottom-sheet/issues/1700))(by [@jaworek](https://github.com/jaworek)) ([a6f44c0](https://github.com/gorhom/react-native-bottom-sheet/commit/a6f44c01ef8f1b9154ce2313614daf075567f641)) +* added support for web without Babel/SWC ([#1741](https://github.com/gorhom/react-native-bottom-sheet/issues/1741))(by [@joshsmith](https://github.com/joshsmith)) ([d620494](https://github.com/gorhom/react-native-bottom-sheet/commit/d620494877e98f4331d8c0a1cb7d375abb06db60)) +* fixed the backdrop tap gesture on web ([#1446](https://github.com/gorhom/react-native-bottom-sheet/issues/1446)) ([b0792de](https://github.com/gorhom/react-native-bottom-sheet/commit/b0792dea5ec605b449d40037cbecfd35bf0ff066)) +* allowed content max height be applied for dynamic sizing ([57c196c](https://github.com/gorhom/react-native-bottom-sheet/commit/57c196cfdf2f63622fb5ea8d6d32cf21b9dd9367)) +* dismiss all action for modals ([#1529](https://github.com/gorhom/react-native-bottom-sheet/issues/1529))(by [@david-gomes5](https://github.com/david-gomes5)) ([17269f1](https://github.com/gorhom/react-native-bottom-sheet/commit/17269f1f55b91f33cec24870ebe00f2510888a4b)) +* fixed position x index sequencing with container resizing ([#1675](https://github.com/gorhom/react-native-bottom-sheet/issues/1675)) ([f0ec705](https://github.com/gorhom/react-native-bottom-sheet/commit/f0ec705cd74ea6e31614ab12c0b4fdc097d3820d)) +* prevent updating backdrop state when unmounting ([#1657](https://github.com/gorhom/react-native-bottom-sheet/issues/1657))(by [@christophby](https://github.com/christophby)) ([d746d85](https://github.com/gorhom/react-native-bottom-sheet/commit/d746d85b92e2bdb4351ea4d3fde140e3199ac671)) +* **web:** use absolute positioning for BottomSheetContainer in web ([#1597](https://github.com/gorhom/react-native-bottom-sheet/issues/1597)) ([d6e3dc9](https://github.com/gorhom/react-native-bottom-sheet/commit/d6e3dc9b327b840895c875dcf016fb5c80a62915)) +* (BottomSheetTextInput): reset shouldHandleKeyboardEvents on unmount (#1495)(by @koplyarov) ([`81cd66f`](https://github.com/gorhom/react-native-bottom-sheet/commit/81cd66f9c49843e43231d1d81ec4aa518a9f1b95)) +* updated containerOffset top value to default to 0 (#1420)(by @beqramo) ([`b81cb93`](https://github.com/gorhom/react-native-bottom-sheet/commit/b81cb9368b55c24703a9c000a76e89a2d253e141)) +* resume close animation when container gets resized (#1374) (#1392) ([`1f69625`](https://github.com/gorhom/react-native-bottom-sheet/commit/1f69625e180fcec4d8d3dec436f8d5bb4eba476b)) +* (bottom-sheet-modal): added container component prop to modal (#1309)(by @magrinj) ([`67e1e09`](https://github.com/gorhom/react-native-bottom-sheet/commit/67e1e09acbc0e96e435a0c2247fa1e0bc19f91aa)) +* updated scrollables mocks with ReactNative list equivalent (#1394)(by @gkueny) ([`630f87f`](https://github.com/gorhom/react-native-bottom-sheet/commit/630f87ff6bd19c4dfc071783139c938eda3baf6c)) +* crash on swipe down (#1367)(by @beqramo) ([`3ccbefc`](https://github.com/gorhom/react-native-bottom-sheet/commit/3ccbefc4d16558867d518f7e0306fbb4d1dbdbeb)) +* (BottomSheetScrollView): updated scroll responders props type (#1335)(by @eps1lon) ([`e42fafc`](https://github.com/gorhom/react-native-bottom-sheet/commit/e42fafcc492d01665c296bf551a6a264eb866fc5)) +* fixed keyboard dismissing issue with Reanimated v3 (#1346)(by @janicduplessis) ([`1d1a464`](https://github.com/gorhom/react-native-bottom-sheet/commit/1d1a46489bede1d3f119df2fb6f467e778461c39)) +- (#1119): fixed race condition between onmount and keyboard animations ([`a1ec74d`](https://github.com/gorhom/react-native-bottom-sheet/commit/a1ec74dbbc85476bb39f3637e9a97214e0cad9a0)) #### Chores And Housekeeping -- updated dependencies ([`7d2a947`](https://github.com/gorhom/react-native-bottom-sheet/commit/7d2a9473a95c3e245e90932715406b62e81e6a63)) -- patch react-native-gesture-handler for android ([`26a0d64`](https://github.com/gorhom/react-native-bottom-sheet/commit/26a0d64a062a441b2f96b3f04c48a039cee6684a)) +* updated expo and react native deps (#1445) ([`f6f2304`](https://github.com/gorhom/react-native-bottom-sheet/commit/f6f2304235c05f92d86ce8083caf910b9297a10a)) +* updated react native and other deps (#1412) ([`549e461`](https://github.com/gorhom/react-native-bottom-sheet/commit/549e461530a91e1d7c95a5178bd2238ebf84df86)) +* fixed types (#1123)(by @stropho) ([`b440964`](https://github.com/gorhom/react-native-bottom-sheet/commit/b44096451d4fed81be7f08b0edf638e4a1c42ccd)) +* updated reanimated to v3 (#1324) ([`4829316`](https://github.com/gorhom/react-native-bottom-sheet/commit/4829316beeff95c9e2efa5fbfdfcf7ef37b4af60)) diff --git a/README.md b/README.md index 6b47d2fcb..419bfa065 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # React Native Bottom Sheet -[![Reanimated v2 version](https://img.shields.io/github/package-json/v/gorhom/react-native-bottom-sheet/master?label=Reanimated%20v2&style=flat-square)](https://www.npmjs.com/package/@gorhom/bottom-sheet) [![Reanimated v1 version](https://img.shields.io/github/package-json/v/gorhom/react-native-bottom-sheet/v2?label=Reanimated%20v1&style=flat-square)](https://www.npmjs.com/package/@gorhom/bottom-sheet) [![npm](https://img.shields.io/npm/l/@gorhom/bottom-sheet?style=flat-square)](https://www.npmjs.com/package/@gorhom/bottom-sheet) [![npm](https://img.shields.io/badge/types-included-blue?style=flat-square)](https://www.npmjs.com/package/@gorhom/bottom-sheet) [![runs with expo](https://img.shields.io/badge/Runs%20with%20Expo-4630EB.svg?style=flat-square&logo=EXPO&labelColor=f3f3f3&logoColor=000)](https://expo.io/) -[![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) +[![Reanimated v3 version](https://img.shields.io/github/package-json/v/gorhom/react-native-bottom-sheet/master?label=Reanimated%20v3&style=flat-square)](https://www.npmjs.com/package/@gorhom/bottom-sheet) [![Reanimated v2 version](https://img.shields.io/github/package-json/v/gorhom/react-native-bottom-sheet/v4?label=Reanimated%20v2&style=flat-square)](https://www.npmjs.com/package/@gorhom/bottom-sheet) [![Reanimated v1 version](https://img.shields.io/github/package-json/v/gorhom/react-native-bottom-sheet/v2?label=Reanimated%20v1&style=flat-square)](https://www.npmjs.com/package/@gorhom/bottom-sheet)
+[![license](https://img.shields.io/npm/l/@gorhom/bottom-sheet?style=flat-square)](https://www.npmjs.com/package/@gorhom/bottom-sheet) [![npm](https://img.shields.io/badge/types-included-blue?style=flat-square)](https://www.npmjs.com/package/@gorhom/bottom-sheet) [![runs with expo](https://img.shields.io/badge/Runs%20with%20Expo-4630EB.svg?style=flat-square&logo=EXPO&labelColor=f3f3f3&logoColor=000)](https://expo.io/)
![NPM Downloads](https://img.shields.io/npm/dw/%40gorhom%2Fbottom-sheet?style=flat-square) - A performant interactive bottom sheet with fully configurable options 🚀 @@ -12,58 +11,41 @@ A performant interactive bottom sheet with fully configurable options 🚀 --- ## Features - -- Modal presentation view, [Bottom Sheet Modal](https://gorhom.github.io/react-native-bottom-sheet/modal). +- ⭐️ Support React Native Web, [read more](https://gorhom.dev/react-native-bottom-sheet/web-support). +- ⭐️ Dynamic Sizing, [read more](https://gorhom.dev/react-native-bottom-sheet/dynamic-sizing). +- ⭐️ Support FlashList, [read more](https://gorhom.dev/react-native-bottom-sheet/components/bottomsheetflashlist). +- Modal presentation view, [Bottom Sheet Modal](https://gorhom.dev/react-native-bottom-sheet/modal). - Smooth gesture interactions & snapping animations. -- Seamless [keyboard handling](https://gorhom.github.io/react-native-bottom-sheet/keyboard-handling) for iOS & Android. -- Support [pull to refresh](https://gorhom.github.io/react-native-bottom-sheet/pull-to-refresh) for scrollables. -- Support [FlatList](https://gorhom.github.io/react-native-bottom-sheet/components/bottomsheetflatlist), [SectionList](https://gorhom.github.io/react-native-bottom-sheet/components/bottomsheetsectionlist), [ScrollView](https://gorhom.github.io/react-native-bottom-sheet/components/bottomsheetscrollview) & [View](https://gorhom.github.io/react-native-bottom-sheet/components/bottomsheetview) scrolling interactions. -- Support [React Navigation integration](https://gorhom.github.io/react-native-bottom-sheet/react-navigation-integration). -- Compatible with `Reanimated` v1 & v2. +- Seamless [keyboard handling](https://gorhom.dev/react-native-bottom-sheet/keyboard-handling) for iOS & Android. +- Support [pull to refresh](https://gorhom.dev/react-native-bottom-sheet/pull-to-refresh) for scrollables. +- Support `FlatList`, `SectionList`, `ScrollView` & `View` scrolling interactions, [read more](https://gorhom.dev/react-native-bottom-sheet/scrollables). +- Support `React Navigation` Integration, [read more](https://gorhom.dev/react-native-bottom-sheet/react-navigation-integration). +- Compatible with `Reanimated` v1-3. - Compatible with `Expo`. - Accessibility support. - Written in `TypeScript`. -- [Read more](https://gorhom.github.io/react-native-bottom-sheet). +- [Read more](https://gorhom.dev/react-native-bottom-sheet). ## Getting Started -Check out [the documentation website](https://gorhom.github.io/react-native-bottom-sheet). +Check out [the documentation website](https://gorhom.dev/react-native-bottom-sheet). ## Versioning -This library been written in 2 versions of `Reanimated`, and kept both implementation in 2 separate branches: +This library been written in 3 versions of `Reanimated`, and kept all implementation in separate branches: + +- **`v5`** | [branch](https://github.com/gorhom/react-native-bottom-sheet/tree/master) | [changelog](https://github.com/gorhom/react-native-bottom-sheet/blob/master/CHANGELOG.md) : written with `Reanimated v3` & `Gesture Handler v2`. -- **`v2`** | [branch](https://github.com/gorhom/react-native-bottom-sheet/tree/v2) | [changelog](https://github.com/gorhom/react-native-bottom-sheet/blob/v2/CHANGELOG.md) : written with `Reanimated v1` & compatible with `Reanimated v2`. +- `v4` (not maintained) | [branch](https://github.com/gorhom/react-native-bottom-sheet/tree/v4) | [changelog](https://github.com/gorhom/react-native-bottom-sheet/blob/v4/CHANGELOG.md) : written with `Reanimated v2`. -- **`v4`** | [branch](https://github.com/gorhom/react-native-bottom-sheet/tree/master) | [changelog](https://github.com/gorhom/react-native-bottom-sheet/blob/master/CHANGELOG.md) : written with `Reanimated v2`. +- `v2` (not maintained) | [branch](https://github.com/gorhom/react-native-bottom-sheet/tree/v2) | [changelog](https://github.com/gorhom/react-native-bottom-sheet/blob/v2/CHANGELOG.md) : written with `Reanimated v1` & compatible with `Reanimated v2`. -> I highly recommend all `v3` users to upgrade to `v4` which provides more stability and all latest features. +> I highly recommend to use `v5` which provides more stability with all latest features. ## Author - [Mo Gorhom](https://gorhom.dev/) -## Contributors - -Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): - -> These people have helped to improve the library, but **DO NOT** maintain it. - - - - - - - - - -

Vojtech Novak

💻

kickbk

🐛 ⚠️
- - - - - - ## Sponsor & Support To keep this library maintained and up-to-date please consider [sponsoring it on GitHub](https://github.com/sponsors/gorhom). Or if you are looking for a private support or help in customizing the experience, then reach out to me on Twitter [@gorhom](https://twitter.com/gorhom). diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..51fda88ac --- /dev/null +++ b/biome.json @@ -0,0 +1,111 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", + "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": false }, + "formatter": { + "enabled": true, + "useEditorconfig": true, + "formatWithErrors": false, + "indentStyle": "space", + "lineEnding": "lf", + "lineWidth": 80, + "ignore": ["**/.github", "**/lib", "**/.expo", "**/website"] + }, + "organizeImports": { "enabled": true, "ignore": ["**/website"] }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noMultipleSpacesInRegularExpressionLiterals": "warn", + "noUselessLoneBlockStatements": "warn", + "noUselessUndefinedInitialization": "warn", + "noVoid": "warn", + "noWith": "warn", + "useLiteralKeys": "warn" + }, + "correctness": { + "noConstAssign": "error", + "noConstantCondition": "off", + "noEmptyCharacterClassInRegex": "warn", + "noGlobalObjectCalls": "warn", + "noInnerDeclarations": "off", + "noInvalidUseBeforeDeclaration": "off", + "noUndeclaredVariables": "error", + "noUnreachable": "error", + "noUnusedVariables": "warn", + "useArrayLiterals": "warn", + "useExhaustiveDependencies": "error", + "useHookAtTopLevel": "error", + "useIsNan": "warn" + }, + "security": { "noGlobalEval": "error" }, + "style": { + "noCommaOperator": "warn", + "noYodaExpression": "warn", + "useBlockStatements": "warn", + "useCollapsedElseIf": "off", + "useConsistentBuiltinInstantiation": "warn", + "useDefaultSwitchClause": "off", + "useSingleVarDeclarator": "off", + "useExponentiationOperator": "off" + }, + "suspicious": { + "noCatchAssign": "warn", + "noCommentText": "error", + "noConsole": { + "level": "error", + "options": { "allow": ["warn", "error"] } + }, + "noControlCharactersInRegex": "warn", + "noDebugger": "warn", + "noDoubleEquals": "warn", + "noDuplicateClassMembers": "error", + "noDuplicateJsxProps": "error", + "noDuplicateObjectKeys": "error", + "noEmptyBlockStatements": "off", + "noFallthroughSwitchClause": "warn", + "noFunctionAssign": "warn", + "noLabelVar": "warn", + "noRedeclare": "off", + "noSelfCompare": "warn", + "noShadowRestrictedNames": "warn", + "noSparseArray": "warn", + "useValidTypeof": "warn" + } + }, + "ignore": ["**/node_modules/", "**/lib", "**/.expo", "**/website"] + }, + "javascript": { + "jsxRuntime": "reactClassic", + "formatter": { + "trailingCommas": "es5", + "semicolons": "always", + "arrowParentheses": "asNeeded", + "quoteStyle": "single", + "bracketSpacing": true + }, + "globals": [ + "clearImmediate", + "queueMicrotask", + "Blob", + "Set", + "Promise", + "requestIdleCallback", + "setImmediate", + "requestAnimationFrame", + "File", + "Map", + "__DEV__", + "WebSocket" + ] + }, + "files": { + "ignore": [ + "**/node_modules/", + "**/lib", + "**/.expo", + "**/example", + "**/website" + ] + } +} diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 000000000..05647d55c --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,35 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo diff --git a/example/App.tsx b/example/App.tsx new file mode 100644 index 000000000..eb1230adc --- /dev/null +++ b/example/App.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import Main from './src/Main'; + +import { enableScreens } from 'react-native-screens'; +enableScreens(true); + +// @ts-ignore +import { enableLogging } from '@gorhom/bottom-sheet'; +enableLogging(); + +export default function App() { + return ( + +
+ + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, +}); diff --git a/example/app.json b/example/app.json new file mode 100644 index 000000000..6e65937a8 --- /dev/null +++ b/example/app.json @@ -0,0 +1,42 @@ +{ + "expo": { + "name": "BottomSheet", + "slug": "BottomSheet", + "githubUrl": "https://github.com/gorhom/react-native-bottom-sheet", + "version": "5.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "automatic", + "backgroundColor": "#000000", + "newArchEnabled": true, + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#000" + }, + "assetBundlePatterns": ["**/*"], + "ios": { + "supportsTablet": true, + "bundleIdentifier": "dev.gorhom.bottomsheet" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#000" + }, + "package": "dev.gorhom.bottomsheet", + "softwareKeyboardLayoutMode": "pan" + }, + "web": { + "favicon": "./assets/favicon.png" + }, + "plugins": [ + [ + "expo-asset", + { + "assets": ["./assets"] + } + ] + ] + } +} diff --git a/example/app/package.json b/example/app/package.json deleted file mode 100644 index 0894dc9f4..000000000 --- a/example/app/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@gorhom/bottom-sheet-example-app", - "description": "Example app for @gorhom/bottom-sheet", - "version": "0.0.1", - "main": "./src/index", - "react-native": "./src/index", - "private": true, - "peerDependencies": { - "@gorhom/portal": "^1.0.13", - "@gorhom/showcase-template": "^2.1.0", - "@react-native-community/blur": "^3.6.0", - "@react-native-community/masked-view": "0.1.11", - "@react-navigation/bottom-tabs": "^6.0.9", - "@react-navigation/elements": "^1.2.1", - "@react-navigation/material-top-tabs": "^6.0.6", - "@react-navigation/native": "^6.0.6", - "@react-navigation/native-stack": "^6.2.5", - "@react-navigation/stack": "^6.0.11", - "faker": "^4.1.0", - "nanoid": "^3.3.3", - "react": "17.0.2", - "react-native": "0.68.1", - "react-native-gesture-handler": "^2.5.0", - "react-native-maps": "^0.30.1", - "react-native-pager-view": "^5.4.24", - "react-native-reanimated": "^2.9.1", - "react-native-redash": "^16.0.11", - "react-native-safe-area-context": "4.2.4", - "react-native-screens": "^3.15.0", - "react-native-tab-view": "^3.1.1", - "@babel/core": "^7.13.10", - "@babel/runtime": "^7.13.10", - "@types/faker": "^4.1.12", - "@types/react": "^17.0.35", - "@types/react-native": "^0.66.5", - "metro-react-native-babel-preset": "^0.67.0", - "typescript": "^4.2.4" - } -} diff --git a/example/app/src/App.tsx b/example/app/src/App.tsx deleted file mode 100644 index afaa3c6e2..000000000 --- a/example/app/src/App.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, { useMemo } from 'react'; -import { StyleSheet } from 'react-native'; -import { ShowcaseApp } from '@gorhom/showcase-template'; -import { screens as defaultScreens } from './screens'; -import { version, description } from '../../../package.json'; -import { GestureHandlerRootView } from 'react-native-gesture-handler'; - -const author = { - username: 'Mo Gorhom', - url: 'https://gorhom.dev', -}; - -interface AppProps { - screens?: any[]; -} - -export const App = ({ screens: providedScreens }: AppProps) => { - const screens = useMemo( - () => [...defaultScreens, ...(providedScreens ? providedScreens : [])], - [providedScreens] - ); - return ( - - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - flexGrow: 1, - }, -}); diff --git a/example/app/src/index.ts b/example/app/src/index.ts deleted file mode 100644 index 118d911fc..000000000 --- a/example/app/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { App } from './App'; -export { default as ModalBackdropExample } from './screens/modal/BackdropExample'; -export { withModalProvider } from './screens/modal/withModalProvider'; -export { Button } from './components/button'; -export { ContactList } from './components/contactList'; -export { ContactItem } from './components/contactItem'; -export { SearchHandle, SEARCH_HANDLE_HEIGHT } from './components/searchHandle'; -export * from './utilities/createMockData'; diff --git a/example/app/src/screens/advanced/DynamicSnapPointExample.tsx b/example/app/src/screens/advanced/DynamicSnapPointExample.tsx deleted file mode 100644 index fb9340225..000000000 --- a/example/app/src/screens/advanced/DynamicSnapPointExample.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { View, StyleSheet, Text } from 'react-native'; -import BottomSheet, { - BottomSheetView, - useBottomSheetDynamicSnapPoints, -} from '@gorhom/bottom-sheet'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { Button } from '../../components/button'; - -const DynamicSnapPointExample = () => { - // state - const [count, setCount] = useState(0); - const initialSnapPoints = useMemo(() => ['CONTENT_HEIGHT'], []); - - // hooks - const { bottom: safeBottomArea } = useSafeAreaInsets(); - const bottomSheetRef = useRef(null); - const { - animatedHandleHeight, - animatedSnapPoints, - animatedContentHeight, - handleContentLayout, - } = useBottomSheetDynamicSnapPoints(initialSnapPoints); - - // callbacks - const handleIncreaseContentPress = useCallback(() => { - setCount(state => state + 1); - }, []); - const handleDecreaseContentPress = useCallback(() => { - setCount(state => Math.max(state - 1, 0)); - }, []); - const handleExpandPress = useCallback(() => { - bottomSheetRef.current?.expand(); - }, []); - const handleClosePress = useCallback(() => { - bottomSheetRef.current?.close(); - }, []); - - // styles - const contentContainerStyle = useMemo( - () => [ - styles.contentContainerStyle, - { paddingBottom: safeBottomArea || 6 }, - ], - [safeBottomArea] - ); - const emojiContainerStyle = useMemo( - () => ({ - ...styles.emojiContainer, - height: 50 * count, - }), - [count] - ); - - // renders - return ( - - +

+ + + + +
+ + diff --git a/example/webpack.config.js b/example/webpack.config.js new file mode 100644 index 000000000..b5e6418a0 --- /dev/null +++ b/example/webpack.config.js @@ -0,0 +1,40 @@ +const createExpoWebpackConfigAsync = require('@expo/webpack-config'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); +const node_modules = path.join(__dirname, 'node_modules'); + +module.exports = async function (env, argv) { + const config = await createExpoWebpackConfigAsync( + { + ...env, + babel: { + dangerouslyAddModulePathsToTranspile: ['react-native-reanimated'], + }, + }, + argv + ); + + config.module.rules.push({ + test: /\.(js|jsx|ts|tsx)$/, + include: path.resolve(root, 'src'), + use: 'babel-loader', + }); + + Object.assign(config.resolve.alias, { + react: path.join(node_modules, 'react'), + 'react-native': path.join(node_modules, 'react-native'), + 'react-native-web': path.join(node_modules, 'react-native-web'), + 'react-native-reanimated': path.join( + node_modules, + 'react-native-reanimated' + ), + 'react-native-gesture-handler': path.join( + node_modules, + 'react-native-gesture-handler' + ), + }); + + // Customize the config before returning it. + return config; +}; diff --git a/lint-staged.config.js b/lint-staged.config.js index 8efb35dc5..46f6524be 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -1,4 +1,13 @@ module.exports = { - '**/*.js': ['eslint'], - '**/*.{ts,tsx}': [() => 'tsc --skipLibCheck --noEmit', 'eslint'], + '*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}': [ + 'biome check --files-ignore-unknown=true', // Check formatting and lint + 'biome check --write --no-errors-on-unmatched', // Format, sort imports, lint, and apply safe fixes + 'biome check --write --organize-imports-enabled=false --no-errors-on-unmatched', // format and apply safe fixes + 'biome check --write --unsafe --no-errors-on-unmatched', // Format, sort imports, lints, apply safe/unsafe fixes + 'biome format --write --no-errors-on-unmatched', // Format + 'biome lint --write --no-errors-on-unmatched', // Lint and apply safe fixes + ], + '*': [ + 'biome check --no-errors-on-unmatched --files-ignore-unknown=true', // Check formatting and lint + ], }; diff --git a/mock.js b/mock.js index d4c85a791..b346057c5 100644 --- a/mock.js +++ b/mock.js @@ -9,9 +9,10 @@ */ const React = require('react'); +const ReactNative = require('react-native'); const NOOP = () => {}; -const NOOP_VALUE = { value: 0 }; +const NOOP_VALUE = { value: 0, set: NOOP, get: () => 0 }; const BottomSheetModalProvider = ({ children }) => { return children; @@ -104,10 +105,16 @@ const useBottomSheetDynamicSnapPoints = () => ({ module.exports = { BottomSheetView: BottomSheetComponent, - BottomSheetScrollView: BottomSheetComponent, - BottomSheetSectionList: BottomSheetComponent, - BottomSheetFlatList: BottomSheetComponent, - BottomSheetVirtualizedList: BottomSheetComponent, + BottomSheetTextInput: ReactNative.TextInput, + BottomSheetScrollView: ReactNative.ScrollView, + BottomSheetSectionList: ReactNative.SectionList, + BottomSheetFlatList: ReactNative.FlatList, + BottomSheetFlashList: ReactNative.FlatList, + BottomSheetVirtualizedList: ReactNative.VirtualizedList, + + TouchableOpacity: ReactNative.TouchableOpacity, + TouchableHighlight: ReactNative.TouchableHighlight, + TouchableWithoutFeedback: ReactNative.TouchableWithoutFeedback, BottomSheetModalProvider, BottomSheetModal, diff --git a/package.json b/package.json index 362ad2e9f..011fee167 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,9 @@ { - "name": "@gorhom/bottom-sheet", - "version": "4.4.5", + "name": "@discord/bottom-sheet", + "version": "5.0.6", "description": "A performant interactive bottom sheet with fully configurable options 🚀", - "main": "lib/commonjs/index", - "module": "lib/module/index", - "types": "lib/typescript/index.d.ts", - "react-native": "src/index.ts", + "main": "src/index.ts", + "source": "src/index.ts", "files": [ "src", "lib", @@ -15,21 +13,22 @@ "react-native", "ios", "android", + "web", "bottom-sheet", "bottomsheet", "reanimated", "sheet" ], - "repository": "https://github.com/gorhom/react-native-bottom-sheet", + "repository": "https://github.com/discord/react-native-bottom-sheet", "author": "Mo Gorhom (https://gorhom.dev)", "license": "MIT", "bugs": { "url": "https://github.com/gorhom/react-native-bottom-sheet/issues" }, - "homepage": "https://gorhom.github.io/react-native-bottom-sheet", + "homepage": "https://gorhom.dev/react-native-bottom-sheet/", "scripts": { "typescript": "tsc --skipLibCheck --noEmit", - "lint": "eslint \"**/*.{js,ts,tsx}\"", + "lint": "biome lint --error-on-warnings ./src", "build": "bob build && yarn copy-dts && yarn delete-dts.js && yarn delete-debug-view", "copy-dts": "copyfiles -u 1 \"src/**/*.d.ts\" lib/typescript", "delete-debug-view": "rm -r ./lib/commonjs/components/bottomSheetDebugView && rm -r ./lib/module/components/bottomSheetDebugView && rm -r ./lib/typescript/components/bottomSheetDebugView", @@ -43,42 +42,44 @@ "invariant": "^2.2.4" }, "devDependencies": { - "@commitlint/cli": "^17.1.2", - "@commitlint/config-conventional": "^17.1.0", - "@react-native-community/eslint-config": "^3.0.0", - "@release-it/conventional-changelog": "^5.1.0", + "@commitlint/cli": "^17.6.5", + "@commitlint/config-conventional": "^17.6.5", + "@release-it/conventional-changelog": "^8.0.1", "@types/invariant": "^2.2.34", - "@types/react": "17.0.2", - "@types/react-native": "^0.67.7", - "auto-changelog": "^2.4.0", + "@types/react": "~18.3.12", + "@types/react-native": "~0.73.0", "copyfiles": "^2.4.1", - "eslint": "^7.32.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-prettier": "^3.4.0", "husky": "^4.3.8", - "lint-staged": "^11.1.2", - "prettier": "^2.3.2", - "react": "~16.9.0", - "react-native": "^0.62.2", - "react-native-builder-bob": "^0.18.1", - "react-native-gesture-handler": "^1.10.3", - "react-native-reanimated": "^2.8.0", - "release-it": "^15.4.2", - "typescript": "^4.2.4" + "lint-staged": "^13.2.2", + "metro-react-native-babel-preset": "^0.77.0", + "react": "18.3.1", + "react-native": "0.76.0", + "react-native-builder-bob": "^0.30.3", + "react-native-gesture-handler": "~2.20.2", + "react-native-reanimated": "~3.16.1", + "release-it": "^17.6.0", + "typescript": "^5.3.0" }, "peerDependencies": { + "@types/react": "*", + "@types/react-native": "*", + "@shopify/flash-list": "*", "react": "*", "react-native": "*", - "react-native-gesture-handler": ">=1.10.1", - "react-native-reanimated": ">=2.2.0" + "react-native-gesture-handler": ">=2.16.1", + "react-native-reanimated": ">=3.16.0" + }, + "peerDependenciesMeta": { + "@types/react-native": { + "optional": true + }, + "@types/react": { + "optional": true + } }, "react-native-builder-bob": { "source": "src", "output": "lib", - "targets": [ - "commonjs", - "module", - "typescript" - ] + "targets": ["commonjs", "module", "typescript"] } } diff --git a/scripts/auto-changelog.js b/scripts/auto-changelog.js deleted file mode 100644 index 24322e430..000000000 --- a/scripts/auto-changelog.js +++ /dev/null @@ -1,44 +0,0 @@ -module.exports = function (Handlebars) { - Handlebars.registerHelper( - 'custom', - function (context, merges, fixes, options) { - if ( - (!context || context.length === 0) && - (!merges || merges.length === 0) && - (!fixes || fixes.length === 0) - ) { - return ''; - } - - const list = [...context, ...(merges || []), ...(fixes || [])] - .filter(item => { - const commit = item.commit || item; - if (options.hash.exclude) { - const pattern = new RegExp(options.hash.exclude, 'm'); - if (pattern.test(commit.message)) { - return false; - } - } - if (options.hash.message) { - const pattern = new RegExp(options.hash.message, 'm'); - return pattern.test(commit.message); - } - if (options.hash.subject) { - const pattern = new RegExp(options.hash.subject); - return pattern.test(commit.subject); - } - return true; - }) - .map(item => options.fn(item.commit || item)) - .join(''); - - if (!list) { - return ''; - } - - return options.hash.heading - ? `${options.hash.heading}\n\n${list}` - : `${list}`; - } - ); -}; diff --git a/src/components/bottomSheet/BottomSheet.tsx b/src/components/bottomSheet/BottomSheet.tsx index b16644070..255458795 100644 --- a/src/components/bottomSheet/BottomSheet.tsx +++ b/src/components/bottomSheet/BottomSheet.tsx @@ -1,3 +1,4 @@ +import invariant from 'invariant'; import React, { useMemo, useCallback, @@ -6,8 +7,8 @@ import React, { memo, useEffect, } from 'react'; -import { Platform } from 'react-native'; -import invariant from 'invariant'; +import { type Insets, Platform } from 'react-native'; +import { State } from 'react-native-gesture-handler'; import Animated, { useAnimatedReaction, useSharedValue, @@ -15,69 +16,76 @@ import Animated, { useDerivedValue, runOnJS, interpolate, - Extrapolate, + Extrapolation, runOnUI, cancelAnimation, useWorkletCallback, - WithSpringConfig, - WithTimingConfig, + type WithSpringConfig, + type WithTimingConfig, + type SharedValue, + useReducedMotion, + ReduceMotion, } from 'react-native-reanimated'; -import { State } from 'react-native-gesture-handler'; -import { - useScrollable, - usePropsValidator, - useReactiveSharedValue, - useNormalizedSnapPoints, - useKeyboard, -} from '../../hooks'; -import { - BottomSheetInternalProvider, - BottomSheetProvider, -} from '../../contexts'; -import BottomSheetContainer from '../bottomSheetContainer'; -import BottomSheetGestureHandlersProvider from '../bottomSheetGestureHandlersProvider'; -import BottomSheetBackdropContainer from '../bottomSheetBackdropContainer'; -import BottomSheetHandleContainer from '../bottomSheetHandleContainer'; -import BottomSheetBackgroundContainer from '../bottomSheetBackgroundContainer'; -import BottomSheetFooterContainer from '../bottomSheetFooterContainer/BottomSheetFooterContainer'; -import BottomSheetDraggableView from '../bottomSheetDraggableView'; -// import BottomSheetDebugView from '../bottomSheetDebugView'; import { + ANIMATION_SOURCE, ANIMATION_STATE, - KEYBOARD_STATE, KEYBOARD_BEHAVIOR, - SHEET_STATE, - SCROLLABLE_STATE, KEYBOARD_BLUR_BEHAVIOR, KEYBOARD_INPUT_MODE, - ANIMATION_SOURCE, + KEYBOARD_STATE, + SCROLLABLE_STATE, + SHEET_STATE, + SNAP_POINT_TYPE, } from '../../constants'; +import { + BottomSheetInternalProvider, + BottomSheetProvider, +} from '../../contexts'; +import type { BottomSheetInternalContextType } from '../../contexts/internal'; +import { + useAnimatedSnapPoints, + useKeyboard, + usePropsValidator, + useReactiveSharedValue, + useScrollable, +} from '../../hooks'; +import type { BottomSheetMethods } from '../../types'; import { animate, getKeyboardAnimationConfigs, normalizeSnapPoint, print, } from '../../utilities'; +import BottomSheetBackdropContainer from '../bottomSheetBackdropContainer'; +import BottomSheetBackgroundContainer from '../bottomSheetBackgroundContainer'; +import BottomSheetContainer from '../bottomSheetContainer'; +// import BottomSheetDebugView from '../bottomSheetDebugView'; +import BottomSheetDraggableView from '../bottomSheetDraggableView'; +import BottomSheetFooterContainer from '../bottomSheetFooterContainer/BottomSheetFooterContainer'; +import BottomSheetGestureHandlersProvider from '../bottomSheetGestureHandlersProvider'; +import BottomSheetHandleContainer from '../bottomSheetHandleContainer'; import { - DEFAULT_OVER_DRAG_RESISTANCE_FACTOR, + DEFAULT_ACCESSIBILITY_LABEL, + DEFAULT_ACCESSIBILITY_ROLE, + DEFAULT_ACCESSIBLE, + DEFAULT_ANIMATE_ON_MOUNT, + DEFAULT_DYNAMIC_SIZING, DEFAULT_ENABLE_CONTENT_PANNING_GESTURE, - DEFAULT_ENABLE_HANDLE_PANNING_GESTURE, DEFAULT_ENABLE_OVER_DRAG, - DEFAULT_ANIMATE_ON_MOUNT, + DEFAULT_ENABLE_PAN_DOWN_TO_CLOSE, DEFAULT_KEYBOARD_BEHAVIOR, DEFAULT_KEYBOARD_BLUR_BEHAVIOR, DEFAULT_KEYBOARD_INPUT_MODE, + DEFAULT_OVER_DRAG_RESISTANCE_FACTOR, INITIAL_CONTAINER_HEIGHT, + INITIAL_CONTAINER_OFFSET, INITIAL_HANDLE_HEIGHT, INITIAL_POSITION, INITIAL_SNAP_POINT, - DEFAULT_ENABLE_PAN_DOWN_TO_CLOSE, - INITIAL_CONTAINER_OFFSET, INITIAL_VALUE, } from './constants'; -import type { BottomSheetMethods, Insets } from '../../types'; -import type { BottomSheetProps, AnimateToPositionType } from './types'; import { styles } from './styles'; +import type { AnimateToPositionType, BottomSheetProps } from './types'; Animated.addWhitelistedUIProps({ decelerationRate: true, @@ -87,10 +95,6 @@ type BottomSheet = BottomSheetMethods; const BottomSheetComponent = forwardRef( function BottomSheet(props, ref) { - //#region validate props - usePropsValidator(props); - //#endregion - //#region extract props const { // animations configurations @@ -99,12 +103,15 @@ const BottomSheetComponent = forwardRef( // configurations index: _providedIndex = 0, snapPoints: _providedSnapPoints, + initialPosition = INITIAL_POSITION, animateOnMount = DEFAULT_ANIMATE_ON_MOUNT, enableContentPanningGesture = DEFAULT_ENABLE_CONTENT_PANNING_GESTURE, - enableHandlePanningGesture = DEFAULT_ENABLE_HANDLE_PANNING_GESTURE, + enableHandlePanningGesture, enableOverDrag = DEFAULT_ENABLE_OVER_DRAG, enablePanDownToClose = DEFAULT_ENABLE_PAN_DOWN_TO_CLOSE, + enableDynamicSizing = DEFAULT_DYNAMIC_SIZING, overDragResistanceFactor = DEFAULT_OVER_DRAG_RESISTANCE_FACTOR, + overrideReduceMotion: _providedOverrideReduceMotion, // styles style: _providedStyle, @@ -122,12 +129,13 @@ const BottomSheetComponent = forwardRef( android_keyboardInputMode = DEFAULT_KEYBOARD_INPUT_MODE, // layout - handleHeight: _providedHandleHeight, containerHeight: _providedContainerHeight, - contentHeight: _providedContentHeight, containerOffset: _providedContainerOffset, topInset = 0, bottomInset = 0, + maxDynamicContentSize, + contentHeight: _providedContentHeight, + handleHeight: _providedHandleHeight, // animated callback shared values animatedPosition: _providedAnimatedPosition, @@ -154,11 +162,31 @@ const BottomSheetComponent = forwardRef( handleComponent, backdropComponent, backgroundComponent, - footerComponent, - children: Content, + renderFooter, + children, + + // accessibility + accessible: _providedAccessible = DEFAULT_ACCESSIBLE, + accessibilityLabel: + _providedAccessibilityLabel = DEFAULT_ACCESSIBILITY_LABEL, + accessibilityRole: + _providedAccessibilityRole = DEFAULT_ACCESSIBILITY_ROLE, } = props; //#endregion + //#region validate props + if (__DEV__) { + // biome-ignore lint/correctness/useHookAtTopLevel: used in development only. + usePropsValidator({ + index: _providedIndex, + snapPoints: _providedSnapPoints, + enableDynamicSizing, + topInset, + bottomInset, + }); + } + //#endregion + //#region layout variables /** * This variable is consider an internal variable, @@ -177,23 +205,28 @@ const BottomSheetComponent = forwardRef( return $modal ? _animatedContainerHeight.value - verticalInset : _animatedContainerHeight.value; - }, [$modal, topInset, bottomInset]); + }, [topInset, bottomInset, $modal, _animatedContainerHeight]); const animatedContainerOffset = useReactiveSharedValue( _providedContainerOffset ?? INITIAL_CONTAINER_OFFSET - ) as Animated.SharedValue; - const animatedHandleHeight = useReactiveSharedValue( + ) as SharedValue>; + const animatedHandleHeight = useReactiveSharedValue( _providedHandleHeight ?? INITIAL_HANDLE_HEIGHT ); const animatedFooterHeight = useSharedValue(0); - const animatedSnapPoints = useNormalizedSnapPoints( - _providedSnapPoints, - animatedContainerHeight, - topInset, - bottomInset, - $modal - ); + const animatedContentHeight = useSharedValue(_providedContentHeight ?? INITIAL_CONTAINER_HEIGHT); + const [animatedSnapPoints, animatedDynamicSnapPointIndex] = + useAnimatedSnapPoints( + _providedSnapPoints, + animatedContainerHeight, + animatedContentHeight, + animatedHandleHeight, + animatedFooterHeight, + enableDynamicSizing, + maxDynamicContentSize + ); const animatedHighestSnapPoint = useDerivedValue( - () => animatedSnapPoints.value[animatedSnapPoints.value.length - 1] + () => animatedSnapPoints.value[animatedSnapPoints.value.length - 1], + [animatedSnapPoints] ); const animatedClosedPosition = useDerivedValue(() => { let closedPosition = animatedContainerHeight.value; @@ -203,16 +236,17 @@ const BottomSheetComponent = forwardRef( } return closedPosition; - }, [$modal, detached, bottomInset]); + }, [animatedContainerHeight, $modal, detached, bottomInset]); const animatedSheetHeight = useDerivedValue( - () => animatedContainerHeight.value - animatedHighestSnapPoint.value + () => animatedContainerHeight.value - animatedHighestSnapPoint.value, + [animatedContainerHeight, animatedHighestSnapPoint] ); const animatedCurrentIndex = useReactiveSharedValue( animateOnMount ? -1 : _providedIndex ); - const animatedPosition = useSharedValue(INITIAL_POSITION); + const animatedPosition = useSharedValue(initialPosition); const animatedNextPosition = useSharedValue(INITIAL_VALUE); - const animatedNextPositionIndex = useSharedValue(0); + const animatedNextPositionIndex = useSharedValue(INITIAL_VALUE); // conditional const isAnimatedOnMount = useSharedValue(false); @@ -232,14 +266,6 @@ const BottomSheetComponent = forwardRef( } let isHandleHeightCalculated = false; - // handle height is provided. - if ( - _providedHandleHeight !== null && - _providedHandleHeight !== undefined && - typeof _providedHandleHeight === 'number' - ) { - isHandleHeightCalculated = true; - } // handle component is null. if (handleComponent === null) { animatedHandleHeight.value = 0; @@ -261,9 +287,16 @@ const BottomSheetComponent = forwardRef( isHandleHeightCalculated && isSnapPointsNormalized ); - }); + }, [ + _providedContainerHeight, + animatedContainerHeight, + animatedHandleHeight, + animatedSnapPoints, + handleComponent, + ]); const isInTemporaryPosition = useSharedValue(false); const isForcedClosing = useSharedValue(false); + const animatedContainerHeightDidChange = useSharedValue(false); // gesture const animatedContentGestureState = useSharedValue( @@ -281,6 +314,8 @@ const BottomSheetComponent = forwardRef( animatedScrollableContentOffsetY, animatedScrollableOverrideState, isScrollableRefreshable, + isScrollableLocked, + isScrollEnded, setScrollableRef, removeScrollableRef, } = useScrollable(); @@ -293,6 +328,13 @@ const BottomSheetComponent = forwardRef( shouldHandleKeyboardEvents, } = useKeyboard(); const animatedKeyboardHeightInContainer = useSharedValue(0); + const userReduceMotionSetting = useReducedMotion(); + const reduceMotion = useMemo(() => { + return !_providedOverrideReduceMotion || + _providedOverrideReduceMotion === ReduceMotion.System + ? userReduceMotionSetting + : _providedOverrideReduceMotion === ReduceMotion.Always; + }, [userReduceMotionSetting, _providedOverrideReduceMotion]); //#endregion //#region state/dynamic variables @@ -303,14 +345,16 @@ const BottomSheetComponent = forwardRef( ); const animatedSheetState = useDerivedValue(() => { // closed position = position >= container height - if (animatedPosition.value >= animatedClosedPosition.value) + if (animatedPosition.value >= animatedClosedPosition.value) { return SHEET_STATE.CLOSED; + } // extended position = container height - sheet height const extendedPosition = animatedContainerHeight.value - animatedSheetHeight.value; - if (animatedPosition.value === extendedPosition) + if (animatedPosition.value === extendedPosition) { return SHEET_STATE.EXTENDED; + } // extended position with keyboard = // container height - (sheet height + keyboard height in root container) @@ -350,7 +394,15 @@ const BottomSheetComponent = forwardRef( isInTemporaryPosition, keyboardBehavior, ]); - const animatedScrollableState = useDerivedValue(() => { + const animatedScrollableState = useDerivedValue(() => { + /** + * if user had disabled content panning gesture, then we unlock + * the scrollable state. + */ + if (!enableContentPanningGesture) { + return SCROLLABLE_STATE.UNLOCKED; + } + /** * if scrollable override state is set, then we just return its value. */ @@ -373,6 +425,13 @@ const BottomSheetComponent = forwardRef( return SCROLLABLE_STATE.UNLOCKED; } + /** + * if the current scrollable is blocked from translation, unlock scrolling + */ + if (!isScrollableLocked.value) { + return SCROLLABLE_STATE.UNLOCKED; + } + /** * if keyboard is shown and sheet is animating * then we do not lock the scrolling to not lose @@ -386,9 +445,15 @@ const BottomSheetComponent = forwardRef( } return SCROLLABLE_STATE.LOCKED; - }); + }, [ + enableContentPanningGesture, + animatedAnimationState, + animatedKeyboardState, + animatedScrollableOverrideState, + animatedSheetState, + ]); // dynamic - const animatedContentHeight = useDerivedValue(() => { + const animatedContentHeightMax = useDerivedValue(() => { const keyboardHeightInContainer = animatedKeyboardHeightInContainer.value; const handleHeight = Math.max(0, animatedHandleHeight.value); let contentHeight = animatedSheetHeight.value - handleHeight; @@ -457,7 +522,7 @@ const BottomSheetComponent = forwardRef( const adjustedSnapPoints = animatedSnapPoints.value.slice().reverse(); const adjustedSnapPointsIndexes = animatedSnapPoints.value .slice() - .map((_: any, index: number) => index) + .map((_, index: number) => index) .reverse(); /** @@ -471,7 +536,7 @@ const BottomSheetComponent = forwardRef( animatedPosition.value, adjustedSnapPoints, adjustedSnapPointsIndexes, - Extrapolate.CLAMP + Extrapolation.CLAMP ) : -1; @@ -500,15 +565,239 @@ const BottomSheetComponent = forwardRef( } return currentIndex; - }, [android_keyboardInputMode]); + }, [ + android_keyboardInputMode, + animatedAnimationSource, + animatedAnimationState, + animatedContainerHeight, + animatedCurrentIndex, + animatedNextPositionIndex, + animatedPosition, + animatedSnapPoints, + isInTemporaryPosition, + isLayoutCalculated, + ]); + //#endregion + + //#region private methods + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheet.name): used for debug only + const handleOnChange = useCallback( + function handleOnChange(index: number, position: number) { + if (__DEV__) { + print({ + component: BottomSheet.name, + method: handleOnChange.name, + category: 'callback', + params: { + index, + animatedCurrentIndex: animatedCurrentIndex.value, + }, + }); + } + + if (!_providedOnChange) { + return; + } + + _providedOnChange( + index, + position, + index === animatedDynamicSnapPointIndex.value + ? SNAP_POINT_TYPE.DYNAMIC + : SNAP_POINT_TYPE.PROVIDED + ); + }, + [_providedOnChange, animatedCurrentIndex, animatedDynamicSnapPointIndex] + ); + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheet.name): used for debug only + const handleOnAnimate = useCallback( + function handleOnAnimate(targetIndex: number, source: ANIMATION_SOURCE) { + if (__DEV__) { + print({ + component: BottomSheet.name, + method: handleOnAnimate.name, + category: 'callback', + params: { + toIndex: targetIndex, + fromIndex: animatedCurrentIndex.value, + }, + }); + } + + if (!_providedOnAnimate) { + return; + } + + _providedOnAnimate(animatedCurrentIndex.value, targetIndex, source); + }, + [_providedOnAnimate, animatedCurrentIndex] + ); + //#endregion + + //#region animation + const stopAnimation = useWorkletCallback(() => { + cancelAnimation(animatedPosition); + animatedAnimationSource.value = ANIMATION_SOURCE.NONE; + animatedAnimationState.value = ANIMATION_STATE.STOPPED; + }, [animatedPosition, animatedAnimationState, animatedAnimationSource]); + const animateToPositionCompleted = useWorkletCallback( + function animateToPositionCompleted(isFinished?: boolean) { + if (!isFinished) { + return; + } + + if (__DEV__) { + runOnJS(print)({ + component: BottomSheet.name, + method: animateToPositionCompleted.name, + params: { + animatedCurrentIndex: animatedCurrentIndex.value, + animatedNextPosition: animatedNextPosition.value, + animatedNextPositionIndex: animatedNextPositionIndex.value, + }, + }); + } + + if (animatedAnimationSource.value === ANIMATION_SOURCE.MOUNT) { + isAnimatedOnMount.value = true; + } + + // reset values + isForcedClosing.value = false; + animatedAnimationSource.value = ANIMATION_SOURCE.NONE; + animatedAnimationState.value = ANIMATION_STATE.STOPPED; + animatedNextPosition.value = INITIAL_VALUE; + animatedNextPositionIndex.value = INITIAL_VALUE; + animatedContainerHeightDidChange.value = false; + } + ); + const animateToPosition: AnimateToPositionType = useWorkletCallback( + function animateToPosition( + position: number, + source: ANIMATION_SOURCE, + velocity = 0, + configs?: WithTimingConfig | WithSpringConfig + ) { + if (__DEV__) { + runOnJS(print)({ + component: BottomSheet.name, + method: animateToPosition.name, + params: { + currentPosition: animatedPosition.value, + nextPosition: position, + }, + }); + } + + if ( + position === animatedPosition.value || + position === undefined || + (animatedAnimationState.value === ANIMATION_STATE.RUNNING && + position === animatedNextPosition.value) + ) { + return; + } + + // stop animation if it is running + if (animatedAnimationState.value === ANIMATION_STATE.RUNNING) { + stopAnimation(); + } + + /** + * set animation state to running, and source + */ + animatedAnimationState.value = ANIMATION_STATE.RUNNING; + animatedAnimationSource.value = source; + + /** + * store next position + */ + animatedNextPosition.value = position; + + /** + * offset the position if keyboard is shown + */ + let offset = 0; + if (animatedKeyboardState.value === KEYBOARD_STATE.SHOWN) { + offset = animatedKeyboardHeightInContainer.value; + } + + animatedNextPositionIndex.value = animatedSnapPoints.value.indexOf( + position + offset + ); + + /** + * fire `onAnimate` callback + */ + runOnJS(handleOnAnimate)(animatedNextPositionIndex.value, source); + + /** + * start animation + */ + animatedPosition.value = animate({ + point: position, + configs: configs || _providedAnimationConfigs, + velocity, + overrideReduceMotion: _providedOverrideReduceMotion, + onComplete: animateToPositionCompleted, + }); + }, + [ + handleOnAnimate, + _providedAnimationConfigs, + _providedOverrideReduceMotion, + ] + ); + /** + * Set to position without animation. + * + * @param targetPosition position to be set. + */ + const setToPosition = useWorkletCallback(function setToPosition( + targetPosition: number + ) { + if ( + targetPosition === animatedPosition.value || + targetPosition === undefined || + (animatedAnimationState.value === ANIMATION_STATE.RUNNING && + targetPosition === animatedNextPosition.value) + ) { + return; + } + + if (__DEV__) { + runOnJS(print)({ + component: BottomSheet.name, + method: setToPosition.name, + params: { + currentPosition: animatedPosition.value, + targetPosition, + }, + }); + } + + /** + * store next position + */ + animatedNextPosition.value = targetPosition; + animatedNextPositionIndex.value = + animatedSnapPoints.value.indexOf(targetPosition); + + stopAnimation(); + + // set values + animatedPosition.value = targetPosition; + animatedContainerHeightDidChange.value = false; + }, []); //#endregion //#region private methods /** - * Calculate the next position based on keyboard state. + * Calculate and evaluate the current position based on multiple + * local states. */ - const getNextPosition = useWorkletCallback( - function getNextPosition() { + const getEvaluatedPosition = useWorkletCallback( + function getEvaluatedPosition(source: ANIMATION_SOURCE) { 'worklet'; const currentIndex = animatedCurrentIndex.value; const snapPoints = animatedSnapPoints.value; @@ -516,9 +805,11 @@ const BottomSheetComponent = forwardRef( const highestSnapPoint = animatedHighestSnapPoint.value; /** - * Handle restore sheet position on blur + * if the keyboard blur behavior is restore and keyboard is hidden, + * then we return the previous snap point. */ if ( + source === ANIMATION_SOURCE.KEYBOARD && keyboardBlurBehavior === KEYBOARD_BLUR_BEHAVIOR.restore && keyboardState === KEYBOARD_STATE.HIDDEN && animatedContentGestureState.value !== State.ACTIVE && @@ -530,7 +821,8 @@ const BottomSheetComponent = forwardRef( } /** - * Handle extend behavior + * if the keyboard appearance behavior is extend and keyboard is shown, + * then we return the heights snap point. */ if ( keyboardBehavior === KEYBOARD_BEHAVIOR.extend && @@ -540,7 +832,8 @@ const BottomSheetComponent = forwardRef( } /** - * Handle full screen behavior + * if the keyboard appearance behavior is fill parent and keyboard is shown, + * then we return 0 ( full screen ). */ if ( keyboardBehavior === KEYBOARD_BEHAVIOR.fillParent && @@ -551,11 +844,18 @@ const BottomSheetComponent = forwardRef( } /** - * handle interactive behavior + * if the keyboard appearance behavior is interactive and keyboard is shown, + * then we return the heights points minus the keyboard in container height. */ if ( keyboardBehavior === KEYBOARD_BEHAVIOR.interactive && - keyboardState === KEYBOARD_STATE.SHOWN + keyboardState === KEYBOARD_STATE.SHOWN && + // ensure that this logic does not run on android + // with resize input mode + !( + Platform.OS === 'android' && + android_keyboardInputMode === 'adjustResize' + ) ) { isInTemporaryPosition.value = true; const keyboardHeightInContainer = @@ -563,10 +863,27 @@ const BottomSheetComponent = forwardRef( return Math.max(0, highestSnapPoint - keyboardHeightInContainer); } + /** + * if the bottom sheet is in temporary position, then we return + * the current position. + */ if (isInTemporaryPosition.value) { return animatedPosition.value; } + /** + * if the bottom sheet did not animate on mount, + * then we return the provided index or the closed position. + */ + if (!isAnimatedOnMount.value) { + return _providedIndex === -1 + ? animatedClosedPosition.value + : snapPoints[_providedIndex]; + } + + /** + * return the current index position. + */ return snapPoints[currentIndex]; }, [ @@ -579,156 +896,139 @@ const BottomSheetComponent = forwardRef( animatedPosition, animatedSnapPoints, isInTemporaryPosition, + isAnimatedOnMount, keyboardBehavior, keyboardBlurBehavior, + _providedIndex, ] ); - const handleOnChange = useCallback( - function handleOnChange(index: number) { - print({ - component: BottomSheet.name, - method: handleOnChange.name, - params: { - index, - animatedCurrentIndex: animatedCurrentIndex.value, - }, - }); - - if (_providedOnChange) { - _providedOnChange(index); - } - }, - [_providedOnChange, animatedCurrentIndex] - ); - const handleOnAnimate = useCallback( - function handleOnAnimate(toPoint: number) { - const snapPoints = animatedSnapPoints.value; - const toIndex = snapPoints.indexOf(toPoint); - - print({ - component: BottomSheet.name, - method: handleOnAnimate.name, - params: { - toIndex, - fromIndex: animatedCurrentIndex.value, - }, - }); - - if (!_providedOnAnimate) { - return; - } - - if (toIndex !== animatedCurrentIndex.value) { - _providedOnAnimate(animatedCurrentIndex.value, toIndex); - } - }, - [_providedOnAnimate, animatedSnapPoints, animatedCurrentIndex] - ); - //#endregion - - //#region animation - const stopAnimation = useWorkletCallback(() => { - cancelAnimation(animatedPosition); - isForcedClosing.value = false; - animatedAnimationSource.value = ANIMATION_SOURCE.NONE; - animatedAnimationState.value = ANIMATION_STATE.STOPPED; - }, [animatedPosition, animatedAnimationState, animatedAnimationSource]); - const animateToPositionCompleted = useWorkletCallback( - function animateToPositionCompleted(isFinished?: boolean) { - isForcedClosing.value = false; - - if (!isFinished) { - return; - } - runOnJS(print)({ - component: BottomSheet.name, - method: animateToPositionCompleted.name, - params: { - animatedCurrentIndex: animatedCurrentIndex.value, - animatedNextPosition: animatedNextPosition.value, - animatedNextPositionIndex: animatedNextPositionIndex.value, - }, - }); - animatedAnimationSource.value = ANIMATION_SOURCE.NONE; - animatedAnimationState.value = ANIMATION_STATE.STOPPED; - animatedNextPosition.value = INITIAL_VALUE; - animatedNextPositionIndex.value = INITIAL_VALUE; - } - ); - const animateToPosition: AnimateToPositionType = useWorkletCallback( - function animateToPosition( - position: number, - source: ANIMATION_SOURCE, - velocity: number = 0, - configs?: WithTimingConfig | WithSpringConfig - ) { - if ( - position === animatedPosition.value || - position === undefined || - (animatedAnimationState.value === ANIMATION_STATE.RUNNING && - position === animatedNextPosition.value) - ) { + /** + * Evaluate the bottom sheet position based based on a event source and other local states. + */ + const evaluatePosition = useWorkletCallback( + function evaluatePosition( + source: ANIMATION_SOURCE, + animationConfigs?: WithSpringConfig | WithTimingConfig + ) { + /** + * if a force closing is running and source not from user, then we early exit + */ + if (isForcedClosing.value && source !== ANIMATION_SOURCE.USER) { return; } - - runOnJS(print)({ - component: BottomSheet.name, - method: animateToPosition.name, - params: { - currentPosition: animatedPosition.value, - position, - velocity, - }, - }); - - stopAnimation(); - /** - * set animation state to running, and source + * when evaluating the position while layout is not calculated, then we early exit till it is. */ - animatedAnimationState.value = ANIMATION_STATE.RUNNING; - animatedAnimationSource.value = source; + if (!isLayoutCalculated.value) { + return; + } + + const proposedPosition = getEvaluatedPosition(source); /** - * store next position + * when evaluating the position while the mount animation not been handled, + * then we evaluate on mount use cases. */ - animatedNextPosition.value = position; - animatedNextPositionIndex.value = - animatedSnapPoints.value.indexOf(position); + if (!isAnimatedOnMount.value) { + /** + * if animate on mount is set to true, then we animate to the propose position, + * else, we set the position with out animation. + */ + if (animateOnMount) { + animateToPosition( + proposedPosition, + ANIMATION_SOURCE.MOUNT, + undefined, + animationConfigs + ); + } else { + setToPosition(proposedPosition); + isAnimatedOnMount.value = true; + } + return; + } /** - * fire `onAnimate` callback + * when evaluating the position while the bottom sheet is animating. */ - runOnJS(handleOnAnimate)(position); + if (animatedAnimationState.value === ANIMATION_STATE.RUNNING) { + /** + * when evaluating the position while the bottom sheet is + * closing, then we force closing the bottom sheet with no animation. + */ + if ( + animatedNextPositionIndex.value === -1 && + !isInTemporaryPosition.value + ) { + setToPosition(animatedClosedPosition.value); + return; + } + + /** + * when evaluating the position while it's animating to + * a position other than the current position, then we + * restart the animation. + */ + if (animatedNextPositionIndex.value !== animatedCurrentIndex.value) { + animateToPosition( + animatedSnapPoints.value[animatedNextPositionIndex.value], + source, + undefined, + animationConfigs + ); + return; + } + } /** - * force animation configs from parameters, if provided + * when evaluating the position while the bottom sheet is in closed + * position and not animating, we re-set the position to closed position. */ - if (configs !== undefined) { - animatedPosition.value = animate({ - point: position, - configs, - velocity, - onComplete: animateToPositionCompleted, - }); - } else { + if ( + animatedAnimationState.value !== ANIMATION_STATE.RUNNING && + animatedCurrentIndex.value === -1 + ) { /** - * use animationConfigs callback, if provided + * early exit if reduce motion is enabled and index is out of sync with position. */ - animatedPosition.value = animate({ - point: position, - velocity, - configs: _providedAnimationConfigs, - onComplete: animateToPositionCompleted, - }); + if ( + reduceMotion && + animatedSnapPoints.value[animatedIndex.value] !== + animatedPosition.value + ) { + return; + } + setToPosition(animatedClosedPosition.value); + return; } + + /** + * when evaluating the position after the container resize, then we + * force the bottom sheet to the proposed position with no + * animation. + */ + if (animatedContainerHeightDidChange.value) { + setToPosition(proposedPosition); + return; + } + + /** + * we fall back to the proposed position. + */ + animateToPosition( + proposedPosition, + source, + undefined, + animationConfigs + ); }, - [handleOnAnimate, _providedAnimationConfigs] + [getEvaluatedPosition, animateToPosition, setToPosition, reduceMotion] ); //#endregion //#region public methods + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheet.name): used for debug only const handleSnapToIndex = useCallback( function handleSnapToIndex( index: number, @@ -741,13 +1041,15 @@ const BottomSheetComponent = forwardRef( snapPoints.length - 1 }` ); - print({ - component: BottomSheet.name, - method: handleSnapToIndex.name, - params: { - index, - }, - }); + if (__DEV__) { + print({ + component: BottomSheet.name, + method: handleSnapToIndex.name, + params: { + index, + }, + }); + } const nextPosition = snapPoints[index]; @@ -793,22 +1095,22 @@ const BottomSheetComponent = forwardRef( position: number | string, animationConfigs?: WithSpringConfig | WithTimingConfig ) { - print({ - component: BottomSheet.name, - method: handleSnapToPosition.name, - params: { - position, - }, - }); + if (__DEV__) { + print({ + component: BottomSheet.name, + method: handleSnapToPosition.name, + params: { + position, + }, + }); + } /** * normalized provided position. */ const nextPosition = normalizeSnapPoint( position, - animatedContainerHeight.value, - topInset, - bottomInset + animatedContainerHeight.value ); /** @@ -847,14 +1149,17 @@ const BottomSheetComponent = forwardRef( animatedPosition, ] ); + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheet.name): used for debug only const handleClose = useCallback( function handleClose( animationConfigs?: WithSpringConfig | WithTimingConfig ) { - print({ - component: BottomSheet.name, - method: handleClose.name, - }); + if (__DEV__) { + print({ + component: BottomSheet.name, + method: handleClose.name, + }); + } const nextPosition = animatedClosedPosition.value; @@ -893,14 +1198,17 @@ const BottomSheetComponent = forwardRef( animatedClosedPosition, ] ); + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheet.name): used for debug only const handleForceClose = useCallback( function handleForceClose( animationConfigs?: WithSpringConfig | WithTimingConfig ) { - print({ - component: BottomSheet.name, - method: handleForceClose.name, - }); + if (__DEV__) { + print({ + component: BottomSheet.name, + method: handleForceClose.name, + }); + } const nextPosition = animatedClosedPosition.value; @@ -941,14 +1249,17 @@ const BottomSheetComponent = forwardRef( animatedClosedPosition, ] ); + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheet.name): used for debug only const handleExpand = useCallback( function handleExpand( animationConfigs?: WithSpringConfig | WithTimingConfig ) { - print({ - component: BottomSheet.name, - method: handleExpand.name, - }); + if (__DEV__) { + print({ + component: BottomSheet.name, + method: handleExpand.name, + }); + } const snapPoints = animatedSnapPoints.value; const nextPosition = snapPoints[snapPoints.length - 1]; @@ -990,14 +1301,17 @@ const BottomSheetComponent = forwardRef( animatedNextPositionIndex, ] ); + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheet.name): used for debug only const handleCollapse = useCallback( function handleCollapse( animationConfigs?: WithSpringConfig | WithTimingConfig ) { - print({ - component: BottomSheet.name, - method: handleCollapse.name, - }); + if (__DEV__) { + print({ + component: BottomSheet.name, + method: handleCollapse.name, + }); + } const nextPosition = animatedSnapPoints.value[0]; @@ -1050,9 +1364,10 @@ const BottomSheetComponent = forwardRef( //#endregion //#region contexts variables - const internalContextVariables = useMemo( + const internalContextVariables = useMemo( () => ({ enableContentPanningGesture, + enableDynamicSizing, overDragResistanceFactor, enableOverDrag, enablePanDownToClose, @@ -1079,6 +1394,8 @@ const BottomSheetComponent = forwardRef( isInTemporaryPosition, isContentHeightFixed, isScrollableRefreshable, + isScrollableLocked, + isScrollEnded, shouldHandleKeyboardEvents, simultaneousHandlers: _providedSimultaneousHandlers, waitFor: _providedWaitFor, @@ -1114,12 +1431,15 @@ const BottomSheetComponent = forwardRef( shouldHandleKeyboardEvents, animatedScrollableContentOffsetY, isScrollableRefreshable, + isScrollableLocked, + isScrollEnded, isContentHeightFixed, isInTemporaryPosition, enableContentPanningGesture, overDragResistanceFactor, enableOverDrag, enablePanDownToClose, + enableDynamicSizing, _providedSimultaneousHandlers, _providedWaitFor, _providedActiveOffsetX, @@ -1159,8 +1479,6 @@ const BottomSheetComponent = forwardRef( //#region styles const containerAnimatedStyle = useAnimatedStyle( () => ({ - opacity: - Platform.OS === 'android' && animatedIndex.value === -1 ? 0 : 1, transform: [ { translateY: animatedPosition.value, @@ -1175,20 +1493,30 @@ const BottomSheetComponent = forwardRef( ); const contentContainerAnimatedStyle = useAnimatedStyle(() => { /** - * if content height was provided, then we skip setting - * calculated height. + * if dynamic sizing is enabled, and content height + * is still not set, then we exit method. */ - if (_providedContentHeight) { + if ( + enableDynamicSizing && + animatedContentHeight.value === INITIAL_CONTAINER_HEIGHT + ) { return {}; } return { height: animate({ - point: animatedContentHeight.value, + point: animatedContentHeightMax.value, configs: _providedAnimationConfigs, + overrideReduceMotion: _providedOverrideReduceMotion, }), }; - }, [animatedContentHeight, _providedContentHeight]); + }, [ + enableDynamicSizing, + animatedContentHeight, + animatedContentHeightMax, + _providedOverrideReduceMotion, + _providedAnimationConfigs, + ]); const contentContainerStyle = useMemo( () => [styles.contentContainer, contentContainerAnimatedStyle], [contentContainerAnimatedStyle] @@ -1207,7 +1535,7 @@ const BottomSheetComponent = forwardRef( return { paddingBottom: animatedContainerHeight.value, }; - }, [detached]); + }, [animatedContainerHeight, detached]); const contentMaskContainerStyle = useMemo( () => [styles.contentMaskContainer, contentMaskContainerAnimatedStyle], [contentMaskContainerAnimatedStyle] @@ -1215,218 +1543,176 @@ const BottomSheetComponent = forwardRef( //#endregion //#region effects + useAnimatedReaction( + () => animatedContainerHeight.value, + (result, previous) => { + if (result === INITIAL_CONTAINER_HEIGHT) { + return; + } + + animatedContainerHeightDidChange.value = result !== previous; + } + ); + /** - * React to `isLayoutCalculated` change, to insure that the sheet will - * appears/mounts only when all layout is been calculated. + * Reaction to the `snapPoints` change, to insure that the sheet position reflect + * to the current point correctly. * - * @alias OnMount + * @alias OnSnapPointsChange */ useAnimatedReaction( - () => isLayoutCalculated.value, - _isLayoutCalculated => { + () => animatedSnapPoints.value, + (result, previous) => { /** - * exit method if: - * - layout is not calculated yet. - * - already did animate on mount. + * if values did not change, and did handle on mount animation + * then we early exit the method. */ - if (!_isLayoutCalculated || isAnimatedOnMount.value) { + if ( + JSON.stringify(result) === JSON.stringify(previous) && + isAnimatedOnMount.value + ) { return; } - let nextPosition; - if (_providedIndex === -1) { - nextPosition = animatedClosedPosition.value; - animatedNextPositionIndex.value = -1; - } else { - nextPosition = animatedSnapPoints.value[_providedIndex]; - } - - runOnJS(print)({ - component: BottomSheet.name, - method: 'useAnimatedReaction::OnMount', - params: { - isLayoutCalculated: _isLayoutCalculated, - animatedSnapPoints: animatedSnapPoints.value, - nextPosition, - }, - }); - /** - * here we exit method early because the next position - * is out of the screen, this happens when `snapPoints` - * still being calculated. + * if layout is not calculated yet, then we exit the method. */ - if ( - nextPosition === INITIAL_POSITION || - nextPosition === animatedClosedPosition.value - ) { - isAnimatedOnMount.value = true; - animatedCurrentIndex.value = _providedIndex; + if (!isLayoutCalculated.value) { return; } - if (animateOnMount) { - animateToPosition(nextPosition, ANIMATION_SOURCE.MOUNT); - } else { - animatedPosition.value = nextPosition; + if (__DEV__) { + runOnJS(print)({ + component: BottomSheet.name, + method: 'useAnimatedReaction::OnSnapPointChange', + category: 'effect', + params: { + result, + }, + }); } - isAnimatedOnMount.value = true; + + evaluatePosition(ANIMATION_SOURCE.SNAP_POINT_CHANGE); }, - [_providedIndex, animateOnMount] + [isLayoutCalculated, animatedSnapPoints] ); /** - * React to `snapPoints` change, to insure that the sheet position reflect - * to the current point correctly. + * Reaction to the keyboard state change. * - * @alias OnSnapPointsChange + * @alias OnKeyboardStateChange */ useAnimatedReaction( () => ({ - snapPoints: animatedSnapPoints.value, - containerHeight: animatedContainerHeight.value, + _keyboardState: animatedKeyboardState.value, + _keyboardHeight: animatedKeyboardHeight.value, }), (result, _previousResult) => { - const { snapPoints, containerHeight } = result; - const _previousSnapPoints = _previousResult?.snapPoints; - const _previousContainerHeight = _previousResult?.containerHeight; + const { _keyboardState, _keyboardHeight } = result; + const _previousKeyboardState = _previousResult?._keyboardState; + const _previousKeyboardHeight = _previousResult?._keyboardHeight; + /** + * if keyboard state is equal to the previous state, then exit the method + */ if ( - JSON.stringify(snapPoints) === JSON.stringify(_previousSnapPoints) || - !isLayoutCalculated.value || - !isAnimatedOnMount.value || - containerHeight <= 0 + _keyboardState === _previousKeyboardState && + _keyboardHeight === _previousKeyboardHeight ) { return; } - runOnJS(print)({ - component: BottomSheet.name, - method: 'useAnimatedReaction::OnSnapPointChange', - params: { - snapPoints, - }, - }); - - let nextPosition; - let animationConfig; - let animationSource = ANIMATION_SOURCE.SNAP_POINT_CHANGE; + /** + * if state is undetermined, then we early exit. + */ + if (_keyboardState === KEYBOARD_STATE.UNDETERMINED) { + return; + } /** - * if snap points changed while sheet is animating, then - * we stop the animation and animate to the updated point. + * if keyboard is hidden by customer gesture, then we early exit. */ if ( + _keyboardState === KEYBOARD_STATE.HIDDEN && animatedAnimationState.value === ANIMATION_STATE.RUNNING && - animatedNextPositionIndex.value !== animatedCurrentIndex.value + animatedAnimationSource.value === ANIMATION_SOURCE.GESTURE ) { - nextPosition = - animatedNextPositionIndex.value !== -1 - ? snapPoints[animatedNextPositionIndex.value] - : animatedNextPosition.value; - } else if (animatedCurrentIndex.value === -1) { - nextPosition = animatedClosedPosition.value; - } else if (isInTemporaryPosition.value) { - nextPosition = getNextPosition(); - } else { - nextPosition = snapPoints[animatedCurrentIndex.value]; + return; + } + + if (__DEV__) { + runOnJS(print)({ + component: BottomSheet.name, + method: 'useAnimatedReaction::OnKeyboardStateChange', + category: 'effect', + params: { + keyboardState: _keyboardState, + keyboardHeight: _keyboardHeight, + }, + }); + } + + /** + * Calculate the keyboard height in the container. + */ + animatedKeyboardHeightInContainer.value = + _keyboardHeight === 0 + ? 0 + : $modal + ? Math.abs( + _keyboardHeight - + Math.abs(bottomInset - animatedContainerOffset.value.bottom) + ) + : Math.abs( + _keyboardHeight - animatedContainerOffset.value.bottom + ); + /** + * if platform is android and the input mode is resize, then exit the method + */ + if ( + (Platform.OS === 'android' && + android_keyboardInputMode === KEYBOARD_INPUT_MODE.adjustResize) || /** - * if snap points changes because of the container height change, - * then we skip the snap animation by setting the duration to 0. + * if the sheet is closing, then exit then method */ - if (containerHeight !== _previousContainerHeight) { - animationSource = ANIMATION_SOURCE.CONTAINER_RESIZE; - animationConfig = { - duration: 0, - }; + animatedNextPositionIndex.value === -1 + ) { + animatedKeyboardHeightInContainer.value = 0; + + if (keyboardBehavior === KEYBOARD_BEHAVIOR.interactive) { + return; } } - animateToPosition(nextPosition, animationSource, 0, animationConfig); - } - ); - - /** - * React to keyboard appearance state. - * - * @alias OnKeyboardStateChange - */ - useAnimatedReaction( - () => ({ - _keyboardState: animatedKeyboardState.value, - _keyboardHeight: animatedKeyboardHeight.value, - }), - (result, _previousResult) => { - const { _keyboardState, _keyboardHeight } = result; - const _previousKeyboardState = _previousResult?._keyboardState; - const _previousKeyboardHeight = _previousResult?._keyboardHeight; /** - * Calculate the keyboard height in the container. + * if user is interacting with sheet, then exit the method */ - animatedKeyboardHeightInContainer.value = $modal - ? Math.abs( - _keyboardHeight - - Math.abs(bottomInset - animatedContainerOffset.value.bottom) - ) - : Math.abs(_keyboardHeight - animatedContainerOffset.value.bottom); - const hasActiveGesture = animatedContentGestureState.value === State.ACTIVE || animatedContentGestureState.value === State.BEGAN || animatedHandleGestureState.value === State.ACTIVE || animatedHandleGestureState.value === State.BEGAN; + if (hasActiveGesture) { + return; + } + /** + * if new keyboard state is hidden and blur behavior is none, then exit the method + */ if ( - /** - * if keyboard state is equal to the previous state, then exit the method - */ - (_keyboardState === _previousKeyboardState && - _keyboardHeight === _previousKeyboardHeight) || - /** - * if user is interacting with sheet, then exit the method - */ - hasActiveGesture || - /** - * if sheet not animated on mount yet, then exit the method - */ - !isAnimatedOnMount.value || - /** - * if new keyboard state is hidden and blur behavior is none, then exit the method - */ - (_keyboardState === KEYBOARD_STATE.HIDDEN && - keyboardBlurBehavior === KEYBOARD_BLUR_BEHAVIOR.none) || - /** - * if platform is android and the input mode is resize, then exit the method - */ - (Platform.OS === 'android' && - keyboardBehavior === KEYBOARD_BEHAVIOR.interactive && - android_keyboardInputMode === KEYBOARD_INPUT_MODE.adjustResize) + _keyboardState === KEYBOARD_STATE.HIDDEN && + keyboardBlurBehavior === KEYBOARD_BLUR_BEHAVIOR.none ) { - animatedKeyboardHeightInContainer.value = 0; return; } - runOnJS(print)({ - component: BottomSheet.name, - method: 'useAnimatedReaction::OnKeyboardStateChange', - params: { - keyboardState: _keyboardState, - keyboardHeight: _keyboardHeight, - }, - }); - - let animationConfigs = getKeyboardAnimationConfigs( + const animationConfigs = getKeyboardAnimationConfigs( keyboardAnimationEasing.value, keyboardAnimationDuration.value ); - const nextPosition = getNextPosition(); - animateToPosition( - nextPosition, - ANIMATION_SOURCE.KEYBOARD, - 0, - animationConfigs - ); + + evaluatePosition(ANIMATION_SOURCE.KEYBOARD, animationConfigs); }, [ $modal, @@ -1435,7 +1721,7 @@ const BottomSheetComponent = forwardRef( keyboardBlurBehavior, android_keyboardInputMode, animatedContainerOffset, - getNextPosition, + getEvaluatedPosition, ] ); @@ -1448,7 +1734,8 @@ const BottomSheetComponent = forwardRef( if (_providedAnimatedPosition) { _providedAnimatedPosition.value = _animatedPosition + topInset; } - } + }, + [] ); /** @@ -1460,7 +1747,8 @@ const BottomSheetComponent = forwardRef( if (_providedAnimatedIndex) { _providedAnimatedIndex.value = _animatedIndex; } - } + }, + [] ); /** @@ -1478,6 +1766,7 @@ const BottomSheetComponent = forwardRef( }), ({ _animatedIndex, + _animatedPosition, _animationState, _contentGestureState, _handleGestureState, @@ -1489,6 +1778,21 @@ const BottomSheetComponent = forwardRef( return; } + /** + * exit the method if index value is not synced with + * position value. + * + * [read more](https://github.com/gorhom/react-native-bottom-sheet/issues/1356) + */ + if ( + animatedNextPosition.value !== INITIAL_VALUE && + animatedNextPositionIndex.value !== INITIAL_VALUE && + (_animatedPosition !== animatedNextPosition.value || + _animatedIndex !== animatedNextPositionIndex.value) + ) { + return; + } + /** * exit the method if animated index value * has fraction, e.g. 1.99, 0.52 @@ -1511,41 +1815,60 @@ const BottomSheetComponent = forwardRef( return; } + /** + * exit the method if the animated index is out of sync with the + * animated position. this happened when the user enable reduce + * motion setting only. + */ + if ( + reduceMotion && + _animatedIndex === animatedCurrentIndex.value && + animatedSnapPoints.value[_animatedIndex] !== _animatedPosition + ) { + return; + } + /** * if the index is not equal to the current index, * than the sheet position had changed and we trigger * the `onChange` callback. */ if (_animatedIndex !== animatedCurrentIndex.value) { - runOnJS(print)({ - component: BottomSheet.name, - method: 'useAnimatedReaction::OnChange', - params: { - animatedCurrentIndex: animatedCurrentIndex.value, - animatedIndex: _animatedIndex, - }, - }); + if (__DEV__) { + runOnJS(print)({ + component: BottomSheet.name, + method: 'useAnimatedReaction::OnChange', + category: 'effect', + params: { + animatedCurrentIndex: animatedCurrentIndex.value, + animatedIndex: _animatedIndex, + }, + }); + } animatedCurrentIndex.value = _animatedIndex; - runOnJS(handleOnChange)(_animatedIndex); + runOnJS(handleOnChange)(_animatedIndex, _animatedPosition); } /** * if index is `-1` than we fire the `onClose` callback. */ if (_animatedIndex === -1 && _providedOnClose) { - runOnJS(print)({ - component: BottomSheet.name, - method: 'useAnimatedReaction::onClose', - params: { - animatedCurrentIndex: animatedCurrentIndex.value, - animatedIndex: _animatedIndex, - }, - }); + if (__DEV__) { + runOnJS(print)({ + component: BottomSheet.name, + method: 'useAnimatedReaction::onClose', + category: 'effect', + params: { + animatedCurrentIndex: animatedCurrentIndex.value, + animatedIndex: _animatedIndex, + }, + }); + } runOnJS(_providedOnClose)(); } }, - [handleOnChange, _providedOnClose] + [reduceMotion, handleOnChange, _providedOnClose] ); /** @@ -1557,24 +1880,13 @@ const BottomSheetComponent = forwardRef( if (isAnimatedOnMount.value) { handleSnapToIndex(_providedIndex); } - }, [ - _providedIndex, - animatedCurrentIndex, - isAnimatedOnMount, - handleSnapToIndex, - ]); + }, [_providedIndex, isAnimatedOnMount, handleSnapToIndex]); //#endregion // render - print({ - component: BottomSheet.name, - method: 'render', - params: { - animatedSnapPoints: animatedSnapPoints.value, - animatedCurrentIndex: animatedCurrentIndex.value, - providedIndex: _providedIndex, - }, - }); + const DraggableView = enableContentPanningGesture + ? BottomSheetDraggableView + : Animated.View; return ( @@ -1608,19 +1920,19 @@ const BottomSheetComponent = forwardRef( - - {typeof Content === 'function' ? : Content} - - {footerComponent && ( - - )} - + {children} + + {renderFooter && ( + + )} ( // topInset, // bottomInset, animatedSheetState, - animatedScrollableState, - animatedScrollableOverrideState, + // animatedScrollableState, + // animatedScrollableOverrideState, // isScrollableRefreshable, // animatedScrollableContentOffsetY, // keyboardState, - // animatedIndex, - // animatedCurrentIndex, - // animatedPosition, - // animatedContainerHeight, - // animatedSheetHeight, - // animatedHandleHeight, - // animatedContentHeight, + animatedIndex, + animatedCurrentIndex, + animatedPosition, + // animatedHandleGestureState, + // animatedContentGestureState, + animatedContainerHeight, + animatedContentHeightMax, + animatedSheetHeight, + animatedHandleHeight, + animatedContentHeight, + animatedFooterHeight, + animatedKeyboardHeight, + animatedKeyboardHeightInContainer, // // keyboardHeight, // isLayoutCalculated, // isContentHeightFixed, diff --git a/src/components/bottomSheet/constants.ts b/src/components/bottomSheet/constants.ts index a08599191..ffc4355bd 100644 --- a/src/components/bottomSheet/constants.ts +++ b/src/components/bottomSheet/constants.ts @@ -13,6 +13,7 @@ const DEFAULT_ENABLE_HANDLE_PANNING_GESTURE = true; const DEFAULT_ENABLE_OVER_DRAG = true; const DEFAULT_ENABLE_PAN_DOWN_TO_CLOSE = false; const DEFAULT_ANIMATE_ON_MOUNT = true; +const DEFAULT_DYNAMIC_SIZING = true; // keyboard const DEFAULT_KEYBOARD_BEHAVIOR = KEYBOARD_BEHAVIOR.interactive; @@ -32,6 +33,11 @@ const INITIAL_CONTAINER_OFFSET = { const INITIAL_HANDLE_HEIGHT = -999; const INITIAL_POSITION = SCREEN_HEIGHT; +// accessibility +const DEFAULT_ACCESSIBLE = true; +const DEFAULT_ACCESSIBILITY_LABEL = 'Bottom Sheet'; +const DEFAULT_ACCESSIBILITY_ROLE = 'adjustable'; + export { DEFAULT_HANDLE_HEIGHT, DEFAULT_OVER_DRAG_RESISTANCE_FACTOR, @@ -39,6 +45,7 @@ export { DEFAULT_ENABLE_HANDLE_PANNING_GESTURE, DEFAULT_ENABLE_OVER_DRAG, DEFAULT_ENABLE_PAN_DOWN_TO_CLOSE, + DEFAULT_DYNAMIC_SIZING, DEFAULT_ANIMATE_ON_MOUNT, // keyboard DEFAULT_KEYBOARD_BEHAVIOR, @@ -51,4 +58,8 @@ export { INITIAL_HANDLE_HEIGHT, INITIAL_SNAP_POINT, INITIAL_VALUE, + // accessibility + DEFAULT_ACCESSIBLE, + DEFAULT_ACCESSIBILITY_LABEL, + DEFAULT_ACCESSIBILITY_ROLE, }; diff --git a/src/components/bottomSheet/styles.ts b/src/components/bottomSheet/styles.ts index 35b0a3965..4d63bc5f2 100644 --- a/src/components/bottomSheet/styles.ts +++ b/src/components/bottomSheet/styles.ts @@ -8,7 +8,9 @@ export const styles = StyleSheet.create({ left: 0, right: 0, }, - contentContainer: {}, + contentContainer: { + overflow: 'visible', + }, contentMaskContainer: { overflow: 'hidden', }, diff --git a/src/components/bottomSheet/types.d.ts b/src/components/bottomSheet/types.d.ts index c489981e1..010318033 100644 --- a/src/components/bottomSheet/types.d.ts +++ b/src/components/bottomSheet/types.d.ts @@ -1,37 +1,33 @@ import type React from 'react'; -import type { ViewStyle, Insets, StyleProp } from 'react-native'; +import type { Insets, StyleProp, ViewStyle } from 'react-native'; +import type { PanGesture } from 'react-native-gesture-handler'; import type { - SharedValue, AnimateStyle, + ReduceMotion, + SharedValue, WithSpringConfig, WithTimingConfig, } from 'react-native-reanimated'; -import type { PanGestureHandlerProps } from 'react-native-gesture-handler'; -import type { BottomSheetHandleProps } from '../bottomSheetHandle'; -import type { BottomSheetBackdropProps } from '../bottomSheetBackdrop'; -import type { BottomSheetBackgroundProps } from '../bottomSheetBackground'; -import type { BottomSheetFooterProps } from '../bottomSheetFooter'; import type { ANIMATION_SOURCE, KEYBOARD_BEHAVIOR, KEYBOARD_BLUR_BEHAVIOR, KEYBOARD_INPUT_MODE, + SNAP_POINT_TYPE, } from '../../constants'; -import type { GestureEventsHandlersHookType } from '../../types'; +import type { + GestureEventsHandlersHookType, + NullableAccessibilityProps, +} from '../../types'; +import type { BottomSheetBackdropProps } from '../bottomSheetBackdrop'; +import type { BottomSheetBackgroundProps } from '../bottomSheetBackground'; +import type { BottomSheetFooterProps } from '../bottomSheetFooter'; +import type { BottomSheetHandleProps } from '../bottomSheetHandle'; export interface BottomSheetProps extends BottomSheetAnimationConfigs, - Partial< - Pick< - PanGestureHandlerProps, - | 'activeOffsetY' - | 'activeOffsetX' - | 'failOffsetY' - | 'failOffsetX' - | 'waitFor' - | 'simultaneousHandlers' - > - > { + Partial, + Omit { //#region configuration /** * Initial snap point index, provide `-1` to initiate bottom sheet in closed state. @@ -42,13 +38,21 @@ export interface BottomSheetProps /** * Points for the bottom sheet to snap to. It accepts array of number, string or mix. * String values should be a percentage. + * + * ⚠️ This prop is required unless you set `enableDynamicSizing` to `true`. * @example * snapPoints={[200, 500]} * snapPoints={[200, '%50']} * snapPoints={['%100']} * @type Array */ - snapPoints: Array | SharedValue>; + snapPoints?: Array | SharedValue>; + /** + * Initial position of the sheet. + * @type number + * @default SCREEN_HEIGHT + */ + initialPosition?: number; /** * Defines how violently sheet has to be stopped while over dragging. * @type number @@ -85,12 +89,28 @@ export interface BottomSheetProps * @default false */ enablePanDownToClose?: boolean; + /** + * Enable dynamic sizing for content view and scrollable content size. + * @type boolean + * @default true + */ + enableDynamicSizing?: boolean; /** * To start the sheet closed and snap to initial index when it's mounted. * @type boolean * @default true */ animateOnMount?: boolean; + /** + * To override the user reduce motion setting. + * - `ReduceMotion.System`: if the `Reduce motion` accessibility setting is enabled on the device, disable the animation. + * - `ReduceMotion.Always`: disable the animation, even if `Reduce motion` accessibility setting is not enabled. + * - `ReduceMotion.Never`: enable the animation, even if `Reduce motion` accessibility setting is enabled. + * @type ReduceMotion + * @see https://docs.swmansion.com/react-native-reanimated/docs/guides/accessibility + * @default ReduceMotion.System + */ + overrideReduceMotion?: ReduceMotion; //#endregion //#region layout @@ -100,7 +120,7 @@ export interface BottomSheetProps * unless `handleHeight` is provided. * @type number */ - handleHeight?: number | SharedValue; + handleHeight?: number; /** * Container height helps to calculate the internal sheet layouts, * if `containerHeight` not provided, the library internally will calculate it, @@ -110,9 +130,9 @@ export interface BottomSheetProps containerHeight?: number | SharedValue; /** * Content height helps dynamic snap points calculation. - * @type number | SharedValue; + * @type number; */ - contentHeight?: number | SharedValue; + contentHeight?: number; /** * Container offset helps to accurately detect container offsets. * @type SharedValue; @@ -133,6 +153,13 @@ export interface BottomSheetProps * @default 0 */ bottomInset?: number; + /** + * Max dynamic content size height to limit the bottom sheet height + * from exceeding a provided size. + * @type number + * @default container height + */ + maxDynamicContentSize?: number; //#endregion //#region keyboard @@ -240,7 +267,7 @@ export interface BottomSheetProps * * @type (index: number) => void; */ - onChange?: (index: number) => void; + onChange?: (index: number, position: number, type: SNAP_POINT_TYPE) => void; /** * Callback when the sheet close. * @@ -250,9 +277,13 @@ export interface BottomSheetProps /** * Callback when the sheet about to animate to a new position. * - * @type (fromIndex: number, toIndex: number) => void; + * @type (fromIndex: number, toIndex: number, source: ANIMATION_SOURCE) => void; */ - onAnimate?: (fromIndex: number, toIndex: number) => void; + onAnimate?: ( + fromIndex: number, + toIndex: number, + source: ANIMATION_SOURCE + ) => void; //#endregion //#region components @@ -276,16 +307,16 @@ export interface BottomSheetProps */ backgroundComponent?: React.FC | null; /** - * Component to be placed as a footer. + * Function to render as the footer. * @see {BottomSheetFooterProps} - * @type React.FC\ + * @type (props: BottomSheetFooterProps) => React.ReactElement | null; */ - footerComponent?: React.FC; + renderFooter?: (props: BottomSheetFooterProps) => React.ReactElement | null; /** * A scrollable node or normal view. - * @type React.ReactNode[] | React.ReactNode + * @type React.ReactNode */ - children: (() => React.ReactNode) | React.ReactNode[] | React.ReactNode; + children: React.ReactNode; //#endregion //#region private @@ -313,3 +344,16 @@ export type AnimateToPositionType = ( velocity?: number, configs?: WithTimingConfig | WithSpringConfig ) => void; + +export type BottomSheetGestureProps = { + activeOffsetX: Parameters[0]; + activeOffsetY: Parameters[0]; + + failOffsetY: Parameters[0]; + failOffsetX: Parameters[0]; + + simultaneousHandlers: Parameters< + PanGesture['simultaneousWithExternalGesture'] + >[0]; + waitFor: Parameters[0]; +}; diff --git a/src/components/bottomSheetBackdrop/BottomSheetBackdrop.tsx b/src/components/bottomSheetBackdrop/BottomSheetBackdrop.tsx index 35597ce60..3b9bf8797 100644 --- a/src/components/bottomSheetBackdrop/BottomSheetBackdrop.tsx +++ b/src/components/bottomSheetBackdrop/BottomSheetBackdrop.tsx @@ -1,23 +1,30 @@ -import React, { memo, useCallback, useMemo, useState } from 'react'; -import { ViewProps } from 'react-native'; +import React, { + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import type { ViewProps } from 'react-native'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { interpolate, - Extrapolate, useAnimatedStyle, useAnimatedReaction, - useAnimatedGestureHandler, runOnJS, + Extrapolation, } from 'react-native-reanimated'; -import { - TapGestureHandler, - TapGestureHandlerGestureEvent, -} from 'react-native-gesture-handler'; import { useBottomSheet } from '../../hooks'; import { - DEFAULT_OPACITY, + DEFAULT_ACCESSIBILITY_HINT, + DEFAULT_ACCESSIBILITY_LABEL, + DEFAULT_ACCESSIBILITY_ROLE, + DEFAULT_ACCESSIBLE, DEFAULT_APPEARS_ON_INDEX, DEFAULT_DISAPPEARS_ON_INDEX, DEFAULT_ENABLE_TOUCH_THROUGH, + DEFAULT_OPACITY, DEFAULT_PRESS_BEHAVIOR, } from './constants'; import { styles } from './styles'; @@ -33,9 +40,14 @@ const BottomSheetBackdropComponent = ({ onPress, style, children, + accessible: _providedAccessible = DEFAULT_ACCESSIBLE, + accessibilityRole: _providedAccessibilityRole = DEFAULT_ACCESSIBILITY_ROLE, + accessibilityLabel: _providedAccessibilityLabel = DEFAULT_ACCESSIBILITY_LABEL, + accessibilityHint: _providedAccessibilityHint = DEFAULT_ACCESSIBILITY_HINT, }: BottomSheetDefaultBackdropProps) => { //#region hooks const { snapToIndex, close } = useBottomSheet(); + const isMounted = useRef(false); //#endregion //#region defaults @@ -67,34 +79,35 @@ const BottomSheetBackdropComponent = ({ }, [snapToIndex, close, disappearsOnIndex, pressBehavior, onPress]); const handleContainerTouchability = useCallback( (shouldDisableTouchability: boolean) => { - setPointerEvents(shouldDisableTouchability ? 'none' : 'auto'); + isMounted.current && + setPointerEvents(shouldDisableTouchability ? 'none' : 'auto'); }, [] ); //#endregion //#region tap gesture - const gestureHandler = - useAnimatedGestureHandler( - { - onFinish: () => { - runOnJS(handleOnPress)(); - }, - }, - [handleOnPress] - ); + const tapHandler = useMemo(() => { + const gesture = Gesture.Tap().onEnd(() => { + runOnJS(handleOnPress)(); + }); + return gesture; + }, [handleOnPress]); //#endregion //#region styles - const containerAnimatedStyle = useAnimatedStyle(() => ({ - opacity: interpolate( - animatedIndex.value, - [-1, disappearsOnIndex, appearsOnIndex], - [0, 0, opacity], - Extrapolate.CLAMP - ), - flex: 1, - })); + const containerAnimatedStyle = useAnimatedStyle( + () => ({ + opacity: interpolate( + animatedIndex.value, + [-1, disappearsOnIndex, appearsOnIndex], + [0, 0, opacity], + Extrapolation.CLAMP + ), + flex: 1, + }), + [animatedIndex, appearsOnIndex, disappearsOnIndex, opacity] + ); const containerStyle = useMemo( () => [styles.container, style, containerAnimatedStyle], [style, containerAnimatedStyle] @@ -112,28 +125,41 @@ const BottomSheetBackdropComponent = ({ }, [disappearsOnIndex] ); + + // addressing updating the state after unmounting. + // [link](https://github.com/gorhom/react-native-bottom-sheet/issues/1376) + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); //#endregion - return pressBehavior !== 'none' ? ( - - - {children} - - - ) : ( - + const AnimatedView = ( + {children} ); + + return pressBehavior !== 'none' ? ( + {AnimatedView} + ) : ( + AnimatedView + ); }; const BottomSheetBackdrop = memo(BottomSheetBackdropComponent); diff --git a/src/components/bottomSheetBackdrop/constants.ts b/src/components/bottomSheetBackdrop/constants.ts index c2388dbb7..bf6f23dd8 100644 --- a/src/components/bottomSheetBackdrop/constants.ts +++ b/src/components/bottomSheetBackdrop/constants.ts @@ -4,10 +4,19 @@ const DEFAULT_DISAPPEARS_ON_INDEX = 0; const DEFAULT_ENABLE_TOUCH_THROUGH = false; const DEFAULT_PRESS_BEHAVIOR = 'close' as const; +const DEFAULT_ACCESSIBLE = true; +const DEFAULT_ACCESSIBILITY_ROLE = 'button'; +const DEFAULT_ACCESSIBILITY_LABEL = 'Bottom sheet backdrop'; +const DEFAULT_ACCESSIBILITY_HINT = 'Tap to close the bottom sheet'; + export { DEFAULT_OPACITY, DEFAULT_APPEARS_ON_INDEX, DEFAULT_DISAPPEARS_ON_INDEX, DEFAULT_ENABLE_TOUCH_THROUGH, DEFAULT_PRESS_BEHAVIOR, + DEFAULT_ACCESSIBLE, + DEFAULT_ACCESSIBILITY_ROLE, + DEFAULT_ACCESSIBILITY_LABEL, + DEFAULT_ACCESSIBILITY_HINT, }; diff --git a/src/components/bottomSheetBackdrop/types.d.ts b/src/components/bottomSheetBackdrop/types.d.ts index 6a33b9a73..f03d6c3d2 100644 --- a/src/components/bottomSheetBackdrop/types.d.ts +++ b/src/components/bottomSheetBackdrop/types.d.ts @@ -1,6 +1,9 @@ -import { ReactNode } from 'react'; +import type { ReactNode } from 'react'; import type { ViewProps } from 'react-native'; -import type { BottomSheetVariables } from '../../types'; +import type { + BottomSheetVariables, + NullableAccessibilityProps, +} from '../../types'; export interface BottomSheetBackdropProps extends Pick, @@ -9,7 +12,8 @@ export interface BottomSheetBackdropProps export type BackdropPressBehavior = 'none' | 'close' | 'collapse' | number; export interface BottomSheetDefaultBackdropProps - extends BottomSheetBackdropProps { + extends BottomSheetBackdropProps, + NullableAccessibilityProps { /** * Backdrop opacity. * @type number diff --git a/src/components/bottomSheetBackdropContainer/BottomSheetBackdropContainer.tsx b/src/components/bottomSheetBackdropContainer/BottomSheetBackdropContainer.tsx index 50daa6f57..d08e51bb6 100644 --- a/src/components/bottomSheetBackdropContainer/BottomSheetBackdropContainer.tsx +++ b/src/components/bottomSheetBackdropContainer/BottomSheetBackdropContainer.tsx @@ -1,6 +1,6 @@ import React, { memo } from 'react'; -import type { BottomSheetBackdropContainerProps } from './types'; import { styles } from './styles'; +import type { BottomSheetBackdropContainerProps } from './types'; const BottomSheetBackdropContainerComponent = ({ animatedIndex, diff --git a/src/components/bottomSheetBackground/BottomSheetBackground.tsx b/src/components/bottomSheetBackground/BottomSheetBackground.tsx index 71ce0c37a..3f165ddff 100644 --- a/src/components/bottomSheetBackground/BottomSheetBackground.tsx +++ b/src/components/bottomSheetBackground/BottomSheetBackground.tsx @@ -1,7 +1,7 @@ import React, { memo } from 'react'; import { View } from 'react-native'; -import type { BottomSheetBackgroundProps } from './types'; import { styles } from './styles'; +import type { BottomSheetBackgroundProps } from './types'; const BottomSheetBackgroundComponent = ({ pointerEvents, diff --git a/src/components/bottomSheetBackgroundContainer/BottomSheetBackgroundContainer.tsx b/src/components/bottomSheetBackgroundContainer/BottomSheetBackgroundContainer.tsx index 80a859ef6..64e9ffc9b 100644 --- a/src/components/bottomSheetBackgroundContainer/BottomSheetBackgroundContainer.tsx +++ b/src/components/bottomSheetBackgroundContainer/BottomSheetBackgroundContainer.tsx @@ -1,8 +1,8 @@ import React, { memo, useMemo } from 'react'; +import { StyleSheet } from 'react-native'; import BottomSheetBackground from '../bottomSheetBackground'; -import type { BottomSheetBackgroundContainerProps } from './types'; import { styles } from './styles'; -import { StyleSheet } from 'react-native'; +import type { BottomSheetBackgroundContainerProps } from './types'; const BottomSheetBackgroundContainerComponent = ({ animatedIndex, diff --git a/src/components/bottomSheetContainer/BottomSheetContainer.tsx b/src/components/bottomSheetContainer/BottomSheetContainer.tsx index 9d42ef185..0716ecdbc 100644 --- a/src/components/bottomSheetContainer/BottomSheetContainer.tsx +++ b/src/components/bottomSheetContainer/BottomSheetContainer.tsx @@ -1,10 +1,10 @@ import React, { memo, useCallback, useMemo, useRef } from 'react'; import { - LayoutChangeEvent, + type LayoutChangeEvent, StatusBar, - StyleProp, + type StyleProp, View, - ViewStyle, + type ViewStyle, } from 'react-native'; import { WINDOW_HEIGHT } from '../../constants'; import { print } from '../../utilities'; @@ -48,13 +48,17 @@ function BottomSheetContainerComponent({ containerRef.current?.measure( (_x, _y, _width, _height, _pageX, pageY) => { + if (!containerOffset.value) { + return; + } containerOffset.value = { - top: pageY, + top: pageY ?? 0, left: 0, right: 0, bottom: Math.max( 0, - WINDOW_HEIGHT - (pageY + height + (StatusBar.currentHeight ?? 0)) + WINDOW_HEIGHT - + ((pageY ?? 0) + height + (StatusBar.currentHeight ?? 0)) ), }; } @@ -63,12 +67,13 @@ function BottomSheetContainerComponent({ print({ component: BottomSheetContainer.displayName, method: 'handleContainerLayout', + category: 'layout', params: { height, }, }); }, - [containerHeight, containerOffset, containerRef] + [containerHeight, containerOffset] ); //#endregion @@ -79,8 +84,9 @@ function BottomSheetContainerComponent({ pointerEvents="box-none" onLayout={shouldCalculateHeight ? handleContainerLayout : undefined} style={containerStyle} - children={children} - /> + > + {children} +
); //#endregion } diff --git a/src/components/bottomSheetContainer/styles.ts b/src/components/bottomSheetContainer/styles.ts index 04f247de5..4968b3731 100644 --- a/src/components/bottomSheetContainer/styles.ts +++ b/src/components/bottomSheetContainer/styles.ts @@ -1,7 +1,8 @@ import { StyleSheet } from 'react-native'; export const styles = StyleSheet.create({ - container: { - ...StyleSheet.absoluteFillObject, + root: { + flex: 1, }, + container: StyleSheet.absoluteFillObject, }); diff --git a/src/components/bottomSheetContainer/styles.web.ts b/src/components/bottomSheetContainer/styles.web.ts index 086ed0d0c..42836a23f 100644 --- a/src/components/bottomSheetContainer/styles.web.ts +++ b/src/components/bottomSheetContainer/styles.web.ts @@ -2,8 +2,7 @@ import { StyleSheet } from 'react-native'; export const styles = StyleSheet.create({ container: { - // @ts-ignore - position: 'fixed', + position: 'absolute', left: 0, right: 0, bottom: 0, diff --git a/src/components/bottomSheetContainer/types.d.ts b/src/components/bottomSheetContainer/types.d.ts index a2193eddb..f3bbdf2a7 100644 --- a/src/components/bottomSheetContainer/types.d.ts +++ b/src/components/bottomSheetContainer/types.d.ts @@ -1,15 +1,15 @@ import type { ReactNode } from 'react'; import type { Insets, StyleProp, ViewStyle } from 'react-native'; -import type Animated from 'react-native-reanimated'; +import type { SharedValue } from 'react-native-reanimated'; import type { BottomSheetProps } from '../bottomSheet/types'; export interface BottomSheetContainerProps extends Partial< Pick > { - containerHeight: Animated.SharedValue; - containerOffset: Animated.SharedValue; + containerHeight: SharedValue; + containerOffset: SharedValue>; shouldCalculateHeight?: boolean; style?: StyleProp; - children: ReactNode; + children?: ReactNode; } diff --git a/src/components/bottomSheetDebugView/BottomSheetDebugView.tsx b/src/components/bottomSheetDebugView/BottomSheetDebugView.tsx index 8919606cb..1836465a5 100644 --- a/src/components/bottomSheetDebugView/BottomSheetDebugView.tsx +++ b/src/components/bottomSheetDebugView/BottomSheetDebugView.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { View } from 'react-native'; -import Animated from 'react-native-reanimated'; +import type { SharedValue } from 'react-native-reanimated'; import ReText from './ReText'; import { styles } from './styles'; interface BottomSheetDebugViewProps { - values: Record | number>; + values: Record | number>; } const BottomSheetDebugView = ({ values }: BottomSheetDebugViewProps) => { diff --git a/src/components/bottomSheetDebugView/ReText.tsx b/src/components/bottomSheetDebugView/ReText.tsx index 9d7b1cdb7..b49a9f5ba 100644 --- a/src/components/bottomSheetDebugView/ReText.tsx +++ b/src/components/bottomSheetDebugView/ReText.tsx @@ -1,32 +1,38 @@ import React from 'react'; -import { TextProps as RNTextProps, TextInput } from 'react-native'; +import { type TextProps as RNTextProps, TextInput } from 'react-native'; import Animated, { + type SharedValue, + type AnimatedProps, useAnimatedProps, useDerivedValue, } from 'react-native-reanimated'; interface TextProps { text: string; - value: Animated.SharedValue | number; - style?: Animated.AnimateProps['style']; + value: SharedValue | number; + style?: AnimatedProps['style']; } const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); +Animated.addWhitelistedNativeProps({ text: true }); + const ReText = (props: TextProps) => { const { text, value: _providedValue, style } = { style: {}, ...props }; - const providedValue = useDerivedValue(() => - typeof _providedValue === 'number' - ? _providedValue - : typeof _providedValue.value === 'number' - ? _providedValue.value.toFixed(2) - : _providedValue.value + const providedValue = useDerivedValue( + () => + typeof _providedValue === 'number' + ? _providedValue + : typeof _providedValue.value === 'number' + ? _providedValue.value.toFixed(2) + : _providedValue.value, + [_providedValue] ); const animatedProps = useAnimatedProps(() => { return { text: `${text}: ${providedValue.value}`, }; - }, [providedValue]); + }, [text, providedValue]); return ( | number; + style?: Animated.AnimateProps['style']; +} + +const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); + +const ReText = (props: TextProps) => { + const { text, value: _providedValue, style } = { style: {}, ...props }; + const textRef = useRef(null); + + const providedValue = useDerivedValue(() => { + const value = + typeof _providedValue === 'number' + ? _providedValue + : typeof _providedValue.value === 'number' + ? _providedValue.value.toFixed(2) + : _providedValue.value; + + return `${text}: ${value}`; + }, [_providedValue, text]); + + //region effects + useAnimatedReaction( + () => providedValue.value, + result => { + textRef.current?.setNativeProps({ + text: result, + }); + }, + [] + ); + //endregion + + return ( + + ); +}; + +export default ReText; diff --git a/src/components/bottomSheetDebugView/styles.web.ts b/src/components/bottomSheetDebugView/styles.web.ts new file mode 100644 index 000000000..d77bfdc0b --- /dev/null +++ b/src/components/bottomSheetDebugView/styles.web.ts @@ -0,0 +1,20 @@ +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + container: { + position: 'absolute', + left: 4, + top: 80, + padding: 2, + width: 400, + backgroundColor: 'rgba(0, 0,0,0.5)', + }, + text: { + fontSize: 14, + lineHeight: 16, + textAlignVertical: 'center', + height: 20, + padding: 0, + color: 'white', + }, +}); diff --git a/src/components/bottomSheetDraggableView/BottomSheetDraggableView.tsx b/src/components/bottomSheetDraggableView/BottomSheetDraggableView.tsx index 377b2d5a8..d721fee7a 100644 --- a/src/components/bottomSheetDraggableView/BottomSheetDraggableView.tsx +++ b/src/components/bottomSheetDraggableView/BottomSheetDraggableView.tsx @@ -1,15 +1,14 @@ -import React, { useMemo, useRef, memo } from 'react'; +import React, { useMemo, memo } from 'react'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; -import { PanGestureHandler } from 'react-native-gesture-handler'; +import { BottomSheetDraggableContext } from '../../contexts/gesture'; import { useBottomSheetGestureHandlers, useBottomSheetInternal, } from '../../hooks'; -import { GESTURE_SOURCE } from '../../constants'; import type { BottomSheetDraggableViewProps } from './types'; const BottomSheetDraggableViewComponent = ({ - gestureType = GESTURE_SOURCE.CONTENT, nativeGestureRef, refreshControlGestureRef, style, @@ -26,19 +25,10 @@ const BottomSheetDraggableViewComponent = ({ failOffsetX, failOffsetY, } = useBottomSheetInternal(); - const { contentPanGestureHandler, scrollablePanGestureHandler } = - useBottomSheetGestureHandlers(); + const { contentPanGestureHandler } = useBottomSheetGestureHandlers(); //#endregion //#region variables - const panGestureRef = useRef(null); - const gestureHandler = useMemo( - () => - gestureType === GESTURE_SOURCE.CONTENT - ? contentPanGestureHandler - : scrollablePanGestureHandler, - [gestureType, contentPanGestureHandler, scrollablePanGestureHandler] - ); const simultaneousHandlers = useMemo(() => { const refs = []; @@ -64,25 +54,66 @@ const BottomSheetDraggableViewComponent = ({ nativeGestureRef, refreshControlGestureRef, ]); + const draggableGesture = useMemo(() => { + let gesture = Gesture.Pan() + .enabled(enableContentPanningGesture) + .shouldCancelWhenOutside(false) + .runOnJS(false) + .onStart(contentPanGestureHandler.handleOnStart) + .onChange(contentPanGestureHandler.handleOnChange) + .onEnd(contentPanGestureHandler.handleOnEnd) + .onFinalize(contentPanGestureHandler.handleOnFinalize); + + if (waitFor) { + gesture = gesture.requireExternalGestureToFail(waitFor); + } + + if (simultaneousHandlers) { + gesture = gesture.simultaneousWithExternalGesture( + simultaneousHandlers as never + ); + } + + if (activeOffsetX) { + gesture = gesture.activeOffsetX(activeOffsetX); + } + + if (activeOffsetY) { + gesture = gesture.activeOffsetY(activeOffsetY); + } + + if (failOffsetX) { + gesture = gesture.failOffsetX(failOffsetX); + } + + if (failOffsetY) { + gesture = gesture.failOffsetY(failOffsetY); + } + + return gesture; + }, [ + activeOffsetX, + activeOffsetY, + enableContentPanningGesture, + failOffsetX, + failOffsetY, + simultaneousHandlers, + waitFor, + contentPanGestureHandler.handleOnChange, + contentPanGestureHandler.handleOnEnd, + contentPanGestureHandler.handleOnFinalize, + contentPanGestureHandler.handleOnStart, + ]); //#endregion return ( - - - {children} - - + + + + {children} + + + ); }; diff --git a/src/components/bottomSheetDraggableView/types.d.ts b/src/components/bottomSheetDraggableView/types.d.ts index 8d38987e2..5ed61d78c 100644 --- a/src/components/bottomSheetDraggableView/types.d.ts +++ b/src/components/bottomSheetDraggableView/types.d.ts @@ -1,17 +1,9 @@ -import type { ReactNode, Ref } from 'react'; +import type { ReactNode } from 'react'; import type { ViewProps as RNViewProps } from 'react-native'; -import type { NativeViewGestureHandler } from 'react-native-gesture-handler'; -import type { GESTURE_SOURCE } from '../../constants'; +import type { GestureRef } from 'react-native-gesture-handler/lib/typescript/handlers/gestures/gesture'; export type BottomSheetDraggableViewProps = RNViewProps & { - /** - * Defines the gesture type of the draggable view. - * - * @default GESTURE_SOURCE.CONTENT - * @type GESTURE_SOURCE - */ - gestureType?: GESTURE_SOURCE; - nativeGestureRef?: Ref | null; - refreshControlGestureRef?: Ref | null; + nativeGestureRef?: Exclude; + refreshControlGestureRef?: Exclude; children: ReactNode[] | ReactNode; }; diff --git a/src/components/bottomSheetFooter/BottomSheetFooter.tsx b/src/components/bottomSheetFooter/BottomSheetFooter.tsx index fff8d6448..584030f7a 100644 --- a/src/components/bottomSheetFooter/BottomSheetFooter.tsx +++ b/src/components/bottomSheetFooter/BottomSheetFooter.tsx @@ -1,10 +1,10 @@ import React, { memo, useCallback, useMemo } from 'react'; -import { LayoutChangeEvent } from 'react-native'; +import type { LayoutChangeEvent } from 'react-native'; import Animated, { useAnimatedStyle } from 'react-native-reanimated'; import { KEYBOARD_STATE } from '../../constants'; import { useBottomSheetInternal } from '../../hooks'; -import type { BottomSheetDefaultFooterProps } from './types'; import { styles } from './styles'; +import type { BottomSheetDefaultFooterProps } from './types'; function BottomSheetFooterComponent({ animatedFooterPosition, @@ -56,12 +56,8 @@ function BottomSheetFooterComponent({ //#endregion return children !== null ? ( - - {typeof children === 'function' ? children() : children} + + {children} ) : null; } diff --git a/src/components/bottomSheetFooter/styles.ts b/src/components/bottomSheetFooter/styles.ts index 8d7db06a3..278f702e3 100644 --- a/src/components/bottomSheetFooter/styles.ts +++ b/src/components/bottomSheetFooter/styles.ts @@ -7,5 +7,6 @@ export const styles = StyleSheet.create({ left: 0, right: 0, zIndex: 9999, + pointerEvents: 'box-none', }, }); diff --git a/src/components/bottomSheetFooter/types.d.ts b/src/components/bottomSheetFooter/types.d.ts index f523065f7..597e10996 100644 --- a/src/components/bottomSheetFooter/types.d.ts +++ b/src/components/bottomSheetFooter/types.d.ts @@ -1,14 +1,14 @@ import type { ReactNode } from 'react'; -import { ViewStyle } from 'react-native'; -import type Animated from 'react-native-reanimated'; +import type { ViewStyle } from 'react-native'; +import type { SharedValue } from 'react-native-reanimated'; export interface BottomSheetFooterProps { /** * Calculated footer animated position. * - * @type Animated.SharedValue + * @type SharedValue */ - animatedFooterPosition: Animated.SharedValue; + animatedFooterPosition: SharedValue; } export interface BottomSheetDefaultFooterProps extends BottomSheetFooterProps { @@ -31,7 +31,7 @@ export interface BottomSheetDefaultFooterProps extends BottomSheetFooterProps { /** * Component to be placed in the footer. * - * @type {ReactNode | ReactNode[]} + * @type {ReactNode|ReactNode[]} */ children?: ReactNode | ReactNode[]; } diff --git a/src/components/bottomSheetFooterContainer/BottomSheetFooterContainer.tsx b/src/components/bottomSheetFooterContainer/BottomSheetFooterContainer.tsx index 0ddd8e01a..0d83c7390 100644 --- a/src/components/bottomSheetFooterContainer/BottomSheetFooterContainer.tsx +++ b/src/components/bottomSheetFooterContainer/BottomSheetFooterContainer.tsx @@ -1,11 +1,11 @@ -import React, { memo } from 'react'; +import { memo } from 'react'; import { useDerivedValue } from 'react-native-reanimated'; -import { useBottomSheetInternal } from '../../hooks'; import { KEYBOARD_STATE } from '../../constants'; +import { useBottomSheetInternal } from '../../hooks'; import type { BottomSheetFooterContainerProps } from './types'; const BottomSheetFooterContainerComponent = ({ - footerComponent: FooterComponent, + renderFooter, }: BottomSheetFooterContainerProps) => { //#region hooks const { @@ -46,7 +46,7 @@ const BottomSheetFooterContainerComponent = ({ ]); //#endregion - return ; + return renderFooter({ animatedFooterPosition }); }; const BottomSheetFooterContainer = memo(BottomSheetFooterContainerComponent); diff --git a/src/components/bottomSheetFooterContainer/types.d.ts b/src/components/bottomSheetFooterContainer/types.d.ts index 2ba8cf705..742c560ae 100644 --- a/src/components/bottomSheetFooterContainer/types.d.ts +++ b/src/components/bottomSheetFooterContainer/types.d.ts @@ -1,4 +1,4 @@ import type { BottomSheetProps } from '../bottomSheet'; export interface BottomSheetFooterContainerProps - extends Required> {} + extends Required> {} diff --git a/src/components/bottomSheetGestureHandlersProvider/BottomSheetGestureHandlersProvider.tsx b/src/components/bottomSheetGestureHandlersProvider/BottomSheetGestureHandlersProvider.tsx index e3314b812..d1860b98c 100644 --- a/src/components/bottomSheetGestureHandlersProvider/BottomSheetGestureHandlersProvider.tsx +++ b/src/components/bottomSheetGestureHandlersProvider/BottomSheetGestureHandlersProvider.tsx @@ -1,13 +1,13 @@ import React, { useMemo } from 'react'; +import { useSharedValue } from 'react-native-reanimated'; import { GESTURE_SOURCE } from '../../constants'; +import { BottomSheetGestureHandlersContext } from '../../contexts'; import { - useGestureHandler, useBottomSheetInternal, useGestureEventsHandlersDefault, + useGestureHandler, } from '../../hooks'; -import { BottomSheetGestureHandlersContext } from '../../contexts'; import type { BottomSheetGestureHandlersProviderProps } from './types'; -import { useSharedValue } from 'react-native-reanimated'; const BottomSheetGestureHandlersProvider = ({ gestureEventsHandlersHook: @@ -23,7 +23,7 @@ const BottomSheetGestureHandlersProvider = ({ //#region hooks const { animatedContentGestureState, animatedHandleGestureState } = useBottomSheetInternal(); - const { handleOnStart, handleOnActive, handleOnEnd } = + const { handleOnStart, handleOnChange, handleOnEnd, handleOnFinalize } = useGestureEventsHandlers(); //#endregion @@ -33,17 +33,9 @@ const BottomSheetGestureHandlersProvider = ({ animatedContentGestureState, animatedGestureSource, handleOnStart, - handleOnActive, - handleOnEnd - ); - - const scrollablePanGestureHandler = useGestureHandler( - GESTURE_SOURCE.SCROLLABLE, - animatedContentGestureState, - animatedGestureSource, - handleOnStart, - handleOnActive, - handleOnEnd + handleOnChange, + handleOnEnd, + handleOnFinalize ); const handlePanGestureHandler = useGestureHandler( @@ -51,8 +43,9 @@ const BottomSheetGestureHandlersProvider = ({ animatedHandleGestureState, animatedGestureSource, handleOnStart, - handleOnActive, - handleOnEnd + handleOnChange, + handleOnEnd, + handleOnFinalize ); //#endregion @@ -61,15 +54,9 @@ const BottomSheetGestureHandlersProvider = ({ () => ({ contentPanGestureHandler, handlePanGestureHandler, - scrollablePanGestureHandler, animatedGestureSource, }), - [ - contentPanGestureHandler, - handlePanGestureHandler, - scrollablePanGestureHandler, - animatedGestureSource, - ] + [contentPanGestureHandler, handlePanGestureHandler, animatedGestureSource] ); //#endregion return ( diff --git a/src/components/bottomSheetHandle/BottomSheetHandle.tsx b/src/components/bottomSheetHandle/BottomSheetHandle.tsx index ea9800571..5e85ec6ae 100644 --- a/src/components/bottomSheetHandle/BottomSheetHandle.tsx +++ b/src/components/bottomSheetHandle/BottomSheetHandle.tsx @@ -1,5 +1,11 @@ import React, { memo, useMemo } from 'react'; import Animated from 'react-native-reanimated'; +import { + DEFAULT_ACCESSIBILITY_HINT, + DEFAULT_ACCESSIBILITY_LABEL, + DEFAULT_ACCESSIBILITY_ROLE, + DEFAULT_ACCESSIBLE, +} from './constants'; import { styles } from './styles'; import type { BottomSheetDefaultHandleProps } from './types'; @@ -7,6 +13,10 @@ const BottomSheetHandleComponent = ({ style, indicatorStyle: _indicatorStyle, children, + accessible = DEFAULT_ACCESSIBLE, + accessibilityRole = DEFAULT_ACCESSIBILITY_ROLE, + accessibilityLabel = DEFAULT_ACCESSIBILITY_LABEL, + accessibilityHint = DEFAULT_ACCESSIBILITY_HINT, }: BottomSheetDefaultHandleProps) => { // styles const containerStyle = useMemo( @@ -23,7 +33,13 @@ const BottomSheetHandleComponent = ({ // render return ( - + {children} diff --git a/src/components/bottomSheetHandle/constants.ts b/src/components/bottomSheetHandle/constants.ts new file mode 100644 index 000000000..98d76c1a8 --- /dev/null +++ b/src/components/bottomSheetHandle/constants.ts @@ -0,0 +1,12 @@ +const DEFAULT_ACCESSIBLE = true; +const DEFAULT_ACCESSIBILITY_ROLE = 'adjustable'; +const DEFAULT_ACCESSIBILITY_LABEL = 'Bottom sheet handle'; +const DEFAULT_ACCESSIBILITY_HINT = + 'Drag up or down to extend or minimize the bottom sheet'; + +export { + DEFAULT_ACCESSIBLE, + DEFAULT_ACCESSIBILITY_ROLE, + DEFAULT_ACCESSIBILITY_LABEL, + DEFAULT_ACCESSIBILITY_HINT, +}; diff --git a/src/components/bottomSheetHandle/types.d.ts b/src/components/bottomSheetHandle/types.d.ts index 20cc2bc20..b36cf5968 100644 --- a/src/components/bottomSheetHandle/types.d.ts +++ b/src/components/bottomSheetHandle/types.d.ts @@ -1,11 +1,16 @@ import type React from 'react'; import type { ViewProps } from 'react-native'; import type { AnimateProps } from 'react-native-reanimated'; -import type { BottomSheetVariables } from '../../types'; +import type { + BottomSheetVariables, + NullableAccessibilityProps, +} from '../../types'; export interface BottomSheetHandleProps extends BottomSheetVariables {} -export interface BottomSheetDefaultHandleProps extends BottomSheetHandleProps { +export interface BottomSheetDefaultHandleProps + extends BottomSheetHandleProps, + NullableAccessibilityProps { /** * View style to be applied to the handle container. * @type Animated.AnimateStyle | ViewStyle diff --git a/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx b/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx index 2219e0f1d..691ab7112 100644 --- a/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx +++ b/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx @@ -1,20 +1,22 @@ import React, { memo, useCallback, useMemo } from 'react'; import type { LayoutChangeEvent } from 'react-native'; -import { PanGestureHandler } from 'react-native-gesture-handler'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; -import BottomSheetHandle from '../bottomSheetHandle'; import { useBottomSheetGestureHandlers, useBottomSheetInternal, } from '../../hooks'; import { print } from '../../utilities'; +import { DEFAULT_ENABLE_HANDLE_PANNING_GESTURE } from '../bottomSheet/constants'; +import BottomSheetHandle from '../bottomSheetHandle'; +import { styles } from './styles'; import type { BottomSheetHandleContainerProps } from './types'; function BottomSheetHandleContainerComponent({ animatedIndex, animatedPosition, simultaneousHandlers: _internalSimultaneousHandlers, - enableHandlePanningGesture, + enableHandlePanningGesture = DEFAULT_ENABLE_HANDLE_PANNING_GESTURE, handleHeight, handleComponent: _providedHandleComponent, handleStyle: _providedHandleStyle, @@ -33,7 +35,7 @@ function BottomSheetHandleContainerComponent({ //#endregion //#region variables - const simultaneousHandlers = useMemo(() => { + const simultaneousHandlers = useMemo(() => { const refs = []; if (_internalSimultaneousHandlers) { @@ -50,6 +52,57 @@ function BottomSheetHandleContainerComponent({ return refs; }, [_providedSimultaneousHandlers, _internalSimultaneousHandlers]); + + const panGesture = useMemo(() => { + let gesture = Gesture.Pan() + .enabled(enableHandlePanningGesture) + .shouldCancelWhenOutside(false) + .runOnJS(false) + .onStart(handlePanGestureHandler.handleOnStart) + .onChange(handlePanGestureHandler.handleOnChange) + .onEnd(handlePanGestureHandler.handleOnEnd) + .onFinalize(handlePanGestureHandler.handleOnFinalize); + + if (waitFor) { + gesture = gesture.requireExternalGestureToFail(waitFor); + } + + if (simultaneousHandlers) { + gesture = gesture.simultaneousWithExternalGesture( + simultaneousHandlers as never + ); + } + + if (activeOffsetX) { + gesture = gesture.activeOffsetX(activeOffsetX); + } + + if (activeOffsetY) { + gesture = gesture.activeOffsetY(activeOffsetY); + } + + if (failOffsetX) { + gesture = gesture.failOffsetX(failOffsetX); + } + + if (failOffsetY) { + gesture = gesture.failOffsetY(failOffsetY); + } + + return gesture; + }, [ + activeOffsetX, + activeOffsetY, + enableHandlePanningGesture, + failOffsetX, + failOffsetY, + simultaneousHandlers, + waitFor, + handlePanGestureHandler.handleOnChange, + handlePanGestureHandler.handleOnEnd, + handlePanGestureHandler.handleOnFinalize, + handlePanGestureHandler.handleOnStart, + ]); //#endregion //#region callbacks @@ -61,13 +114,16 @@ function BottomSheetHandleContainerComponent({ }: LayoutChangeEvent) { handleHeight.value = height; - print({ - component: BottomSheetHandleContainer.displayName, - method: 'handleContainerLayout', - params: { - height, - }, - }); + if (__DEV__) { + print({ + component: BottomSheetHandleContainer.displayName, + method: 'handleContainerLayout', + category: 'layout', + params: { + height, + }, + }); + } }, [handleHeight] ); @@ -79,24 +135,11 @@ function BottomSheetHandleContainerComponent({ ? BottomSheetHandle : _providedHandleComponent; return HandleComponent !== null ? ( - + - + ) : null; //#endregion } diff --git a/src/components/bottomSheetHandleContainer/styles.ts b/src/components/bottomSheetHandleContainer/styles.ts new file mode 100644 index 000000000..c20ba5fca --- /dev/null +++ b/src/components/bottomSheetHandleContainer/styles.ts @@ -0,0 +1,5 @@ +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + container: {}, +}); diff --git a/src/components/bottomSheetHandleContainer/styles.web.ts b/src/components/bottomSheetHandleContainer/styles.web.ts new file mode 100644 index 000000000..30edc7fd7 --- /dev/null +++ b/src/components/bottomSheetHandleContainer/styles.web.ts @@ -0,0 +1,8 @@ +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + container: { + // @ts-ignore + cursor: 'grab', + }, +}); diff --git a/src/components/bottomSheetHandleContainer/types.d.ts b/src/components/bottomSheetHandleContainer/types.d.ts index c3c2ae928..fb535c17e 100644 --- a/src/components/bottomSheetHandleContainer/types.d.ts +++ b/src/components/bottomSheetHandleContainer/types.d.ts @@ -1,8 +1,8 @@ import type { PanGestureHandlerProperties } from 'react-native-gesture-handler'; -import type Animated from 'react-native-reanimated'; +import type { SharedValue } from 'react-native-reanimated'; +import type { useInteractivePanGestureHandlerConfigs } from '../../hooks/useGestureHandler'; import type { BottomSheetProps } from '../bottomSheet'; import type { BottomSheetHandleProps } from '../bottomSheetHandle'; -import type { useInteractivePanGestureHandlerConfigs } from '../../hooks/useGestureHandler'; export interface BottomSheetHandleContainerProps extends Pick, @@ -21,5 +21,5 @@ export interface BottomSheetHandleContainerProps | 'keyboardBehavior' >, BottomSheetHandleProps { - handleHeight: Animated.SharedValue; + handleHeight: SharedValue; } diff --git a/src/components/bottomSheetModal/BottomSheetModal.tsx b/src/components/bottomSheetModal/BottomSheetModal.tsx index e0070c5ce..99bdeb294 100644 --- a/src/components/bottomSheetModal/BottomSheetModal.tsx +++ b/src/components/bottomSheetModal/BottomSheetModal.tsx @@ -1,50 +1,57 @@ +import { Portal, usePortal } from '@gorhom/portal'; import React, { forwardRef, memo, + type RefObject, useCallback, useImperativeHandle, useMemo, useRef, useState, } from 'react'; -import { Portal, usePortal } from '@gorhom/portal'; -import BottomSheet from '../bottomSheet'; +import type { ANIMATION_SOURCE, SNAP_POINT_TYPE } from '../../constants'; import { useBottomSheetModalInternal } from '../../hooks'; +import type { BottomSheetMethods, BottomSheetModalMethods } from '../../types'; import { print } from '../../utilities'; +import { id } from '../../utilities/id'; +import BottomSheet from '../bottomSheet'; import { - DEFAULT_STACK_BEHAVIOR, DEFAULT_ENABLE_DISMISS_ON_CLOSE, + DEFAULT_STACK_BEHAVIOR, } from './constants'; -import type { BottomSheetModalMethods, BottomSheetMethods } from '../../types'; -import type { BottomSheetModalProps } from './types'; -import { id } from '../../utilities/id'; +import type { + BottomSheetModalPrivateMethods, + BottomSheetModalProps, + BottomSheetModalState, +} from './types'; -type BottomSheetModal = BottomSheetModalMethods; - -const INITIAL_STATE: { - mount: boolean; - data: any; -} = { +const INITIAL_STATE: BottomSheetModalState = { mount: false, data: undefined, }; -const BottomSheetModalComponent = forwardRef< - BottomSheetModal, - BottomSheetModalProps ->(function BottomSheetModal(props, ref) { +// biome-ignore lint/suspicious/noExplicitAny: Using 'any' allows users to define their own strict types for 'data' property. +type BottomSheetModal = BottomSheetModalMethods; + +// biome-ignore lint/suspicious/noExplicitAny: Using 'any' allows users to define their own strict types for 'data' property. +function BottomSheetModalComponent( + props: BottomSheetModalProps, + ref: React.ForwardedRef> +) { const { // modal props name, stackBehavior = DEFAULT_STACK_BEHAVIOR, enableDismissOnClose = DEFAULT_ENABLE_DISMISS_ON_CLOSE, onDismiss: _providedOnDismiss, + onAnimate: _providedOnAnimate, // bottom sheet props index = 0, snapPoints, enablePanDownToClose = true, animateOnMount = true, + containerComponent: ContainerComponent = React.Fragment, // callbacks onChange: _providedOnChange, @@ -55,7 +62,8 @@ const BottomSheetModalComponent = forwardRef< } = props; //#region state - const [{ mount, data }, setState] = useState(INITIAL_STATE); + const [{ mount, data }, setState] = + useState>(INITIAL_STATE); //#endregion //#region hooks @@ -72,6 +80,7 @@ const BottomSheetModalComponent = forwardRef< //#region refs const bottomSheetRef = useRef(null); const currentIndexRef = useRef(!animateOnMount ? index : -1); + const nextIndexRef = useRef(null); const restoreIndexRef = useRef(-1); const minimized = useRef(false); const forcedDismissed = useRef(false); @@ -84,6 +93,7 @@ const BottomSheetModalComponent = forwardRef< //#endregion //#region private methods + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheetModal.name): used for debug only const resetVariables = useCallback(function resetVariables() { print({ component: BottomSheetModal.name, @@ -95,12 +105,15 @@ const BottomSheetModalComponent = forwardRef< mounted.current = false; forcedDismissed.current = false; }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheetModal.name): used for debug only const unmount = useCallback( function unmount() { - print({ - component: BottomSheetModal.name, - method: unmount.name, - }); + if (__DEV__) { + print({ + component: BottomSheetModal.name, + method: unmount.name, + }); + } const _mounted = mounted.current; // reset variables @@ -142,71 +155,100 @@ const BottomSheetModalComponent = forwardRef< } bottomSheetRef.current?.snapToPosition(...args); }, []); - const handleExpand = useCallback((...args) => { + const handleExpand: BottomSheetMethods['expand'] = useCallback((...args) => { if (minimized.current) { return; } bottomSheetRef.current?.expand(...args); }, []); - const handleCollapse = useCallback((...args) => { - if (minimized.current) { - return; - } - bottomSheetRef.current?.collapse(...args); - }, []); - const handleClose = useCallback((...args) => { + const handleCollapse: BottomSheetMethods['collapse'] = useCallback( + (...args) => { + if (minimized.current) { + return; + } + bottomSheetRef.current?.collapse(...args); + }, + [] + ); + const handleClose: BottomSheetMethods['close'] = useCallback((...args) => { if (minimized.current) { return; } bottomSheetRef.current?.close(...args); }, []); - const handleForceClose = useCallback((...args) => { - if (minimized.current) { - return; - } - bottomSheetRef.current?.forceClose(...args); - }, []); + const handleForceClose: BottomSheetMethods['forceClose'] = useCallback( + (...args) => { + if (minimized.current) { + return; + } + bottomSheetRef.current?.forceClose(...args); + }, + [] + ); //#endregion //#region bottom sheet modal methods + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheetModal.name): used for debug only + // biome-ignore lint/correctness/useExhaustiveDependencies(ref): ref is a stable object const handlePresent = useCallback( - function handlePresent(_data?: any) { + function handlePresent(_data?: T) { requestAnimationFrame(() => { setState({ mount: true, data: _data, }); - mountSheet(key, ref, stackBehavior); + mountSheet( + key, + ref as unknown as RefObject, + stackBehavior + ); + ref; - print({ - component: BottomSheetModal.name, - method: handlePresent.name, - }); + if (__DEV__) { + print({ + component: BottomSheetModal.name, + method: handlePresent.name, + }); + } }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [key, stackBehavior, mountSheet] ); + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheetModal.name): used for debug only const handleDismiss = useCallback( function handleDismiss(animationConfigs) { - print({ - component: BottomSheetModal.name, - method: handleDismiss.name, - params: { - currentIndexRef: currentIndexRef.current, - minimized: minimized.current, - }, - }); + if (__DEV__) { + print({ + component: BottomSheetModal.name, + method: handleDismiss.name, + params: { + currentIndexRef: currentIndexRef.current, + minimized: minimized.current, + }, + }); + } + + const animating = nextIndexRef.current != null; + /** - * if modal is already been dismiss, we exit the method. + * early exit, if not minimized, it is in closed position and not animating */ - if (currentIndexRef.current === -1 && minimized.current === false) { + if ( + currentIndexRef.current === -1 && + minimized.current === false && + !animating + ) { return; } + /** + * unmount and early exit, if minimized or it is in closed position and not animating + */ if ( - minimized.current || - (currentIndexRef.current === -1 && enablePanDownToClose) + !animating && + (minimized.current || + (currentIndexRef.current === -1 && enablePanDownToClose)) ) { unmount(); return; @@ -217,15 +259,18 @@ const BottomSheetModalComponent = forwardRef< }, [willUnmountSheet, unmount, key, enablePanDownToClose] ); + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheetModal.name): used for debug only const handleMinimize = useCallback( function handleMinimize() { - print({ - component: BottomSheetModal.name, - method: handleMinimize.name, - params: { - minimized: minimized.current, - }, - }); + if (__DEV__) { + print({ + component: BottomSheetModal.name, + method: handleMinimize.name, + params: { + minimized: minimized.current, + }, + }); + } if (minimized.current) { return; } @@ -245,15 +290,18 @@ const BottomSheetModalComponent = forwardRef< }, [index] ); + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheetModal.name): used for debug only const handleRestore = useCallback(function handleRestore() { - print({ - component: BottomSheetModal.name, - method: handleRestore.name, - params: { - minimized: minimized.current, - forcedDismissed: forcedDismissed.current, - }, - }); + if (__DEV__) { + print({ + component: BottomSheetModal.name, + method: handleRestore.name, + params: { + minimized: minimized.current, + forcedDismissed: forcedDismissed.current, + }, + }); + } if (!minimized.current || forcedDismissed.current) { return; } @@ -263,16 +311,19 @@ const BottomSheetModalComponent = forwardRef< //#endregion //#region callbacks + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheetModal.name): used for debug only const handlePortalOnUnmount = useCallback( function handlePortalOnUnmount() { - print({ - component: BottomSheetModal.name, - method: handlePortalOnUnmount.name, - params: { - minimized: minimized.current, - forcedDismissed: forcedDismissed.current, - }, - }); + if (__DEV__) { + print({ + component: BottomSheetModal.name, + method: handlePortalOnUnmount.name, + params: { + minimized: minimized.current, + forcedDismissed: forcedDismissed.current, + }, + }); + } /** * if modal is already been dismiss, we exit the method. */ @@ -298,36 +349,58 @@ const BottomSheetModalComponent = forwardRef< if (mounted.current) { render(); } - }, - []); + }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheetModal.name): used for debug only const handleBottomSheetOnChange = useCallback( - function handleBottomSheetOnChange(_index: number) { - print({ - component: BottomSheetModal.name, - method: handleBottomSheetOnChange.name, - params: { - minimized: minimized.current, - forcedDismissed: forcedDismissed.current, - }, - }); + function handleBottomSheetOnChange( + _index: number, + _position: number, + _type: SNAP_POINT_TYPE + ) { + if (__DEV__) { + print({ + component: BottomSheetModal.name, + method: handleBottomSheetOnChange.name, + category: 'callback', + params: { + minimized: minimized.current, + forcedDismissed: forcedDismissed.current, + }, + }); + } currentIndexRef.current = _index; + nextIndexRef.current = null; if (_providedOnChange) { - _providedOnChange(_index); + _providedOnChange(_index, _position, _type); } }, [_providedOnChange] ); + const handleBottomSheetOnAnimate = useCallback( + (fromIndex: number, toIndex: number, source: ANIMATION_SOURCE) => { + nextIndexRef.current = toIndex; + + if (_providedOnAnimate) { + _providedOnAnimate(fromIndex, toIndex, source); + } + }, + [_providedOnAnimate] + ); + // biome-ignore lint/correctness/useExhaustiveDependencies(BottomSheetModal.name): used for debug only const handleBottomSheetOnClose = useCallback( function handleBottomSheetOnClose() { - print({ - component: BottomSheetModal.name, - method: handleBottomSheetOnClose.name, - params: { - minimized: minimized.current, - forcedDismissed: forcedDismissed.current, - }, - }); + if (__DEV__) { + print({ + component: BottomSheetModal.name, + method: handleBottomSheetOnClose.name, + category: 'callback', + params: { + minimized: minimized.current, + forcedDismissed: forcedDismissed.current, + }, + }); + } if (minimized.current) { return; @@ -360,7 +433,6 @@ const BottomSheetModalComponent = forwardRef< //#endregion // render - // console.log('BottomSheetModal', index, mount, data); return mount ? ( - : Content - } - $modal={true} - /> + + + {typeof Content === 'function' ? : Content} + + ) : null; -}); +} -const BottomSheetModal = memo(BottomSheetModalComponent); -BottomSheetModal.displayName = 'BottomSheetModal'; +const BottomSheetModal = memo(forwardRef(BottomSheetModalComponent)) as < + // biome-ignore lint/suspicious/noExplicitAny: Using 'any' allows users to define their own strict types for 'data' property. + T = any, +>( + props: BottomSheetModalProps & { + ref?: React.ForwardedRef>; + } +) => ReturnType>; +( + BottomSheetModal as React.MemoExoticComponent< + (props: BottomSheetModalProps) => React.JSX.Element + > +).displayName = 'BottomSheetModal'; export default BottomSheetModal; diff --git a/src/components/bottomSheetModal/constants.ts b/src/components/bottomSheetModal/constants.ts index 5fdcd0992..d4c34486b 100644 --- a/src/components/bottomSheetModal/constants.ts +++ b/src/components/bottomSheetModal/constants.ts @@ -1,4 +1,4 @@ -const DEFAULT_STACK_BEHAVIOR = 'replace'; +const DEFAULT_STACK_BEHAVIOR = 'switch'; const DEFAULT_ENABLE_DISMISS_ON_CLOSE = true; export { DEFAULT_STACK_BEHAVIOR, DEFAULT_ENABLE_DISMISS_ON_CLOSE }; diff --git a/src/components/bottomSheetModal/types.d.ts b/src/components/bottomSheetModal/types.d.ts index 9d104e74b..9e8e3eeac 100644 --- a/src/components/bottomSheetModal/types.d.ts +++ b/src/components/bottomSheetModal/types.d.ts @@ -1,6 +1,7 @@ import type React from 'react'; -import type { BottomSheetProps } from '../bottomSheet'; +import type { View } from 'react-native'; import type { MODAL_STACK_BEHAVIOR } from '../../constants'; +import type { BottomSheetProps } from '../bottomSheet'; export interface BottomSheetModalPrivateMethods { dismiss: (force?: boolean) => void; @@ -10,21 +11,23 @@ export interface BottomSheetModalPrivateMethods { export type BottomSheetModalStackBehavior = keyof typeof MODAL_STACK_BEHAVIOR; -export interface BottomSheetModalProps +// biome-ignore lint/suspicious/noExplicitAny: Using 'any' allows users to define their own strict types for 'data' property. +export interface BottomSheetModalProps extends Omit { /** * Modal name to help identify the modal for later on. * @type string - * @default nanoid generated unique key. + * @default generated unique key. */ name?: string; /** * Defines the stack behavior when modal mount. - * - `push` it will mount the modal on top of current modal. - * - `replace` it will minimize the current modal then mount the modal. - * @type `push` | `replace` - * @default replace + * - `push` it will mount the modal on top of the current one. + * - `switch` it will minimize the current modal then mount the new one. + * - `replace` it will dismiss the current modal then mount the new one. + * @type `push` | `switch` | `replace` + * @default switch */ stackBehavior?: BottomSheetModalStackBehavior; @@ -35,6 +38,14 @@ export interface BottomSheetModalProps */ enableDismissOnClose?: boolean; + /** + * Add a custom container like FullWindowOverlay + * allow to fix issue like https://github.com/gorhom/react-native-bottom-sheet/issues/832 + * @type React.ComponentType + * @default undefined + */ + containerComponent?: React.ComponentType; + // callbacks /** * Callback when the modal dismissed. @@ -44,10 +55,13 @@ export interface BottomSheetModalProps /** * A scrollable node or normal view. - * @type React.ReactNode[] | React.ReactNode + * @type React.ReactNode[] | React.ReactNode | (({ data: any }?) => React.ReactElement) */ - children: - | (({ data: any }?) => React.ReactNode) - | React.ReactNode[] - | React.ReactNode; + children: React.FC<{ data?: T }> | React.ReactNode[] | React.ReactNode; +} + +// biome-ignore lint/suspicious/noExplicitAny: Using 'any' allows users to define their own strict types for 'data' property. +export interface BottomSheetModalState { + mount: boolean; + data: T | undefined; } diff --git a/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx b/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx index 23a9f4dc9..2327d8a65 100644 --- a/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx +++ b/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx @@ -1,17 +1,20 @@ +import { PortalProvider } from '@gorhom/portal'; import React, { useCallback, useMemo, useRef } from 'react'; import { useSharedValue } from 'react-native-reanimated'; -import { PortalProvider } from '@gorhom/portal'; +import { MODAL_STACK_BEHAVIOR } from '../../constants'; import { - BottomSheetModalProvider, BottomSheetModalInternalProvider, + BottomSheetModalProvider, } from '../../contexts'; -import BottomSheetContainer from '../bottomSheetContainer'; -import { MODAL_STACK_BEHAVIOR } from '../../constants'; import { INITIAL_CONTAINER_HEIGHT, INITIAL_CONTAINER_OFFSET, } from '../bottomSheet/constants'; -import type { BottomSheetModalStackBehavior } from '../bottomSheetModal'; +import BottomSheetContainer from '../bottomSheetContainer'; +import type { + BottomSheetModalPrivateMethods, + BottomSheetModalStackBehavior, +} from '../bottomSheetModal'; import type { BottomSheetModalProviderProps, BottomSheetModalRef, @@ -31,7 +34,11 @@ const BottomSheetModalProviderWrapper = ({ //#region private methods const handleMountSheet = useCallback( - (key: string, ref: any, stackBehavior: BottomSheetModalStackBehavior) => { + ( + key: string, + ref: React.RefObject, + stackBehavior: BottomSheetModalStackBehavior + ) => { const _sheetsQueue = sheetsQueueRef.current.slice(); const sheetIndex = _sheetsQueue.findIndex(item => item.key === key); const sheetOnTop = sheetIndex === _sheetsQueue.length - 1; @@ -50,13 +57,19 @@ const BottomSheetModalProviderWrapper = ({ * - it is not unmounting. * - stack behavior is 'replace'. */ + + /** + * Handle switch or replace stack behaviors, if: + * - a modal is currently presented. + * - it is not unmounting + */ const currentMountedSheet = _sheetsQueue[_sheetsQueue.length - 1]; - if ( - currentMountedSheet && - !currentMountedSheet.willUnmount && - stackBehavior === MODAL_STACK_BEHAVIOR.replace - ) { - currentMountedSheet.ref?.current?.minimize(); + if (currentMountedSheet && !currentMountedSheet.willUnmount) { + if (stackBehavior === MODAL_STACK_BEHAVIOR.replace) { + currentMountedSheet.ref?.current?.dismiss(); + } else if (stackBehavior === MODAL_STACK_BEHAVIOR.switch) { + currentMountedSheet.ref?.current?.minimize(); + } } /** @@ -185,7 +198,6 @@ const BottomSheetModalProviderWrapper = ({ {children} diff --git a/src/components/bottomSheetModalProvider/types.d.ts b/src/components/bottomSheetModalProvider/types.d.ts index 78f2464c6..5f44c310b 100644 --- a/src/components/bottomSheetModalProvider/types.d.ts +++ b/src/components/bottomSheetModalProvider/types.d.ts @@ -1,11 +1,9 @@ -import type { ReactNode } from 'react'; +import type { ReactNode, RefObject } from 'react'; import type { BottomSheetModalPrivateMethods } from '../bottomSheetModal'; export interface BottomSheetModalRef { key: string; - ref: { - current: BottomSheetModalPrivateMethods; - }; + ref: RefObject; willUnmount: boolean; } diff --git a/src/components/bottomSheetRefreshControl/BottomSheetRefreshControl.android.tsx b/src/components/bottomSheetRefreshControl/BottomSheetRefreshControl.android.tsx index be502259c..395fcc5ad 100644 --- a/src/components/bottomSheetRefreshControl/BottomSheetRefreshControl.android.tsx +++ b/src/components/bottomSheetRefreshControl/BottomSheetRefreshControl.android.tsx @@ -1,35 +1,80 @@ -import React, { forwardRef, memo } from 'react'; -import { RefreshControl, RefreshControlProps } from 'react-native'; -import { NativeViewGestureHandler } from 'react-native-gesture-handler'; +import React, { memo, useContext, useMemo } from 'react'; +import { RefreshControl, type RefreshControlProps } from 'react-native'; +import { + Gesture, + GestureDetector, + type SimultaneousGesture, +} from 'react-native-gesture-handler'; import Animated, { useAnimatedProps } from 'react-native-reanimated'; import { SCROLLABLE_STATE } from '../../constants'; +import { BottomSheetDraggableContext } from '../../contexts/gesture'; import { useBottomSheetInternal } from '../../hooks'; const AnimatedRefreshControl = Animated.createAnimatedComponent(RefreshControl); -const BottomSheetRefreshControlComponent = forwardRef< - NativeViewGestureHandler, - RefreshControlProps ->(({ onRefresh, ...rest }, ref) => { - // hooks - const { animatedScrollableState } = useBottomSheetInternal(); +interface BottomSheetRefreshControlProps extends RefreshControlProps { + scrollableGesture: SimultaneousGesture; +} - // variables - const animatedProps = useAnimatedProps(() => ({ - enabled: animatedScrollableState.value === SCROLLABLE_STATE.UNLOCKED, - })); +function BottomSheetRefreshControlComponent({ + onRefresh, + scrollableGesture, + ...rest +}: BottomSheetRefreshControlProps) { + //#region hooks + const draggableGesture = useContext(BottomSheetDraggableContext); + const { animatedScrollableState, enableContentPanningGesture } = + useBottomSheetInternal(); + //#endregion + + if (!draggableGesture && enableContentPanningGesture) { + throw "'BottomSheetRefreshControl' cannot be used out of the BottomSheet!"; + } + + //#region variables + const animatedProps = useAnimatedProps( + () => ({ + enabled: animatedScrollableState.value === SCROLLABLE_STATE.UNLOCKED, + }), + [animatedScrollableState.value] + ); + + const gesture = useMemo( + () => + draggableGesture + ? Gesture.Native() + // @ts-ignore + .simultaneousWithExternalGesture( + ...draggableGesture.toGestureArray(), + ...scrollableGesture.toGestureArray() + ) + .shouldCancelWhenOutside(true) + : undefined, + [draggableGesture, scrollableGesture] + ); + + //#endregion // render + if (gesture) { + return ( + + + + ); + } return ( - - - + ); -}); +} const BottomSheetRefreshControl = memo(BottomSheetRefreshControlComponent); BottomSheetRefreshControl.displayName = 'BottomSheetRefreshControl'; diff --git a/src/components/bottomSheetRefreshControl/index.ts b/src/components/bottomSheetRefreshControl/index.ts index ac07963d2..b716fa281 100644 --- a/src/components/bottomSheetRefreshControl/index.ts +++ b/src/components/bottomSheetRefreshControl/index.ts @@ -1,15 +1,19 @@ import type React from 'react'; import type { RefreshControlProps } from 'react-native'; -import type { NativeViewGestureHandlerProps } from 'react-native-gesture-handler'; +import type { + NativeViewGestureHandlerProps, + SimultaneousGesture, +} from 'react-native-gesture-handler'; import BottomSheetRefreshControl from './BottomSheetRefreshControl'; -export default BottomSheetRefreshControl as any as React.MemoExoticComponent< +export default BottomSheetRefreshControl as never as React.MemoExoticComponent< React.ForwardRefExoticComponent< RefreshControlProps & { + scrollableGesture: SimultaneousGesture; children: React.ReactNode | React.ReactNode[]; } & React.RefAttributes< React.ComponentType< - NativeViewGestureHandlerProps & React.RefAttributes + NativeViewGestureHandlerProps & React.RefAttributes > > > diff --git a/src/components/bottomSheetScrollable/BottomSheetDraggableScrollable.tsx b/src/components/bottomSheetScrollable/BottomSheetDraggableScrollable.tsx new file mode 100644 index 000000000..92d035a9a --- /dev/null +++ b/src/components/bottomSheetScrollable/BottomSheetDraggableScrollable.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { + GestureDetector, + type SimultaneousGesture, +} from 'react-native-gesture-handler'; + +interface BottomSheetDraggableScrollableProps { + scrollableGesture?: SimultaneousGesture; + children: React.ReactNode; +} + +export function BottomSheetDraggableScrollable({ + scrollableGesture, + children, +}: BottomSheetDraggableScrollableProps) { + if (scrollableGesture) { + return ( + {children} + ); + } + + return children; +} diff --git a/src/components/bottomSheetScrollable/BottomSheetFlashList.tsx b/src/components/bottomSheetScrollable/BottomSheetFlashList.tsx new file mode 100644 index 000000000..542460316 --- /dev/null +++ b/src/components/bottomSheetScrollable/BottomSheetFlashList.tsx @@ -0,0 +1,82 @@ +// @ts-ignore +import type { FlashListProps } from '@shopify/flash-list'; +import React, { forwardRef, memo, useMemo } from 'react'; +import { type ScrollViewProps, StyleSheet } from 'react-native'; +import BottomSheetScrollView from './BottomSheetScrollView'; +import type { + BottomSheetFlashListProps, + BottomSheetScrollViewMethods, +} from './types'; + +let FlashList: { + FlashList: React.FC; +}; +// since FlashList is not a dependency for the library +// we try to import it using metro optional import +try { + FlashList = require('@shopify/flash-list') as never; +} catch (_) {} + +const BottomSheetFlashListComponent = forwardRef< + React.FC, + // biome-ignore lint/suspicious/noExplicitAny: to be addressed + BottomSheetFlashListProps +>((props, ref) => { + //#region props + const { + focusHook, + scrollEventsHandlersHook, + enableFooterMarginAdjustment, + ...rest + // biome-ignore lint: to be addressed! + }: any = props; + //#endregion + + useMemo(() => { + if (!FlashList) { + throw 'You need to install FlashList first, `yarn install @shopify/flash-list`'; + } + }, []); + + //#region render + const renderScrollComponent = useMemo( + () => + forwardRef( + // @ts-ignore + ({ data, ...props }, ref) => { + return ( + // @ts-ignore + + ); + } + ), + [focusHook, scrollEventsHandlersHook, enableFooterMarginAdjustment] + ); + return ( + + ); + //#endregion +}); + +export const styles = StyleSheet.create({ + container: { + flex: 1, + overflow: 'visible', + }, +}); + +export const BottomSheetFlashList = memo(BottomSheetFlashListComponent); + +export default BottomSheetFlashList as ( + props: BottomSheetFlashListProps +) => ReturnType; diff --git a/src/components/bottomSheetScrollable/BottomSheetFlatList.tsx b/src/components/bottomSheetScrollable/BottomSheetFlatList.tsx index 6d3c1b797..9bc7c5f6b 100644 --- a/src/components/bottomSheetScrollable/BottomSheetFlatList.tsx +++ b/src/components/bottomSheetScrollable/BottomSheetFlatList.tsx @@ -1,8 +1,5 @@ -import { memo } from 'react'; -import { - FlatList as RNFlatList, - FlatListProps as RNFlatListProps, -} from 'react-native'; +import { type ComponentProps, memo } from 'react'; +import { FlatList as RNFlatList } from 'react-native'; import Animated from 'react-native-reanimated'; import { SCROLLABLE_TYPE } from '../../constants'; import { createBottomSheetScrollableComponent } from './createBottomSheetScrollableComponent'; @@ -12,11 +9,13 @@ import type { } from './types'; const AnimatedFlatList = - Animated.createAnimatedComponent>(RNFlatList); + Animated.createAnimatedComponent>( + RNFlatList + ); const BottomSheetFlatListComponent = createBottomSheetScrollableComponent< BottomSheetFlatListMethods, - BottomSheetFlatListProps + BottomSheetFlatListProps >(SCROLLABLE_TYPE.FLATLIST, AnimatedFlatList); const BottomSheetFlatList = memo(BottomSheetFlatListComponent); diff --git a/src/components/bottomSheetScrollable/BottomSheetScrollView.tsx b/src/components/bottomSheetScrollable/BottomSheetScrollView.tsx index 6463c4ddf..68be6ef5b 100644 --- a/src/components/bottomSheetScrollable/BottomSheetScrollView.tsx +++ b/src/components/bottomSheetScrollable/BottomSheetScrollView.tsx @@ -1,7 +1,7 @@ import { memo } from 'react'; import { ScrollView as RNScrollView, - ScrollViewProps as RNScrollViewProps, + type ScrollViewProps as RNScrollViewProps, } from 'react-native'; import Animated from 'react-native-reanimated'; import { SCROLLABLE_TYPE } from '../../constants'; diff --git a/src/components/bottomSheetScrollable/BottomSheetSectionList.tsx b/src/components/bottomSheetScrollable/BottomSheetSectionList.tsx index 6e046f192..968be38cf 100644 --- a/src/components/bottomSheetScrollable/BottomSheetSectionList.tsx +++ b/src/components/bottomSheetScrollable/BottomSheetSectionList.tsx @@ -1,8 +1,7 @@ -import { memo } from 'react'; +import { type ComponentProps, memo } from 'react'; import { - DefaultSectionT, + type DefaultSectionT, SectionList as RNSectionList, - SectionListProps as RNSectionListProps, } from 'react-native'; import Animated from 'react-native-reanimated'; import { SCROLLABLE_TYPE } from '../../constants'; @@ -13,11 +12,13 @@ import type { } from './types'; const AnimatedSectionList = - Animated.createAnimatedComponent>(RNSectionList); + Animated.createAnimatedComponent>( + RNSectionList + ); const BottomSheetSectionListComponent = createBottomSheetScrollableComponent< BottomSheetSectionListMethods, - BottomSheetSectionListProps + BottomSheetSectionListProps >(SCROLLABLE_TYPE.SECTIONLIST, AnimatedSectionList); const BottomSheetSectionList = memo(BottomSheetSectionListComponent); diff --git a/src/components/bottomSheetScrollable/BottomSheetVirtualizedList.tsx b/src/components/bottomSheetScrollable/BottomSheetVirtualizedList.tsx index df2ad015e..db636bb5c 100644 --- a/src/components/bottomSheetScrollable/BottomSheetVirtualizedList.tsx +++ b/src/components/bottomSheetScrollable/BottomSheetVirtualizedList.tsx @@ -1,8 +1,5 @@ -import { memo } from 'react'; -import { - VirtualizedList as RNVirtualizedList, - VirtualizedListProps as RNVirtualizedListProps, -} from 'react-native'; +import { type ComponentProps, memo } from 'react'; +import { VirtualizedList as RNVirtualizedList } from 'react-native'; import Animated from 'react-native-reanimated'; import { SCROLLABLE_TYPE } from '../../constants'; import { createBottomSheetScrollableComponent } from './createBottomSheetScrollableComponent'; @@ -12,14 +9,14 @@ import type { } from './types'; const AnimatedVirtualizedList = - Animated.createAnimatedComponent>( + Animated.createAnimatedComponent>( RNVirtualizedList ); const BottomSheetVirtualizedListComponent = createBottomSheetScrollableComponent< BottomSheetVirtualizedListMethods, - BottomSheetVirtualizedListProps + BottomSheetVirtualizedListProps >(SCROLLABLE_TYPE.VIRTUALIZEDLIST, AnimatedVirtualizedList); const BottomSheetVirtualizedList = memo(BottomSheetVirtualizedListComponent); diff --git a/src/components/bottomSheetScrollable/ScrollableContainer.android.tsx b/src/components/bottomSheetScrollable/ScrollableContainer.android.tsx new file mode 100644 index 000000000..1f944e06f --- /dev/null +++ b/src/components/bottomSheetScrollable/ScrollableContainer.android.tsx @@ -0,0 +1,55 @@ +import React, { forwardRef } from 'react'; +import type { SimultaneousGesture } from 'react-native-gesture-handler'; +import BottomSheetRefreshControl from '../bottomSheetRefreshControl'; +import { BottomSheetDraggableScrollable } from './BottomSheetDraggableScrollable'; +import { styles } from './styles'; + +interface ScrollableContainerProps { + nativeGesture: SimultaneousGesture; + // biome-ignore lint: to be addressed + refreshControl: any; + // biome-ignore lint: to be addressed + progressViewOffset: any; + // biome-ignore lint: to be addressed + refreshing: any; + // biome-ignore lint: to be addressed + onRefresh: any; + // biome-ignore lint: to be addressed + ScrollableComponent: any; +} + +// biome-ignore lint: to be addressed +export const ScrollableContainer = forwardRef( + function ScrollableContainer( + { + nativeGesture, + refreshControl: _refreshControl, + refreshing, + progressViewOffset, + onRefresh, + ScrollableComponent, + ...rest + }, + ref + ) { + const Scrollable = ( + + + + ); + + return onRefresh ? ( + + {Scrollable} + + ) : ( + Scrollable + ); + } +); diff --git a/src/components/bottomSheetScrollable/ScrollableContainer.tsx b/src/components/bottomSheetScrollable/ScrollableContainer.tsx new file mode 100644 index 000000000..ab3bc59b2 --- /dev/null +++ b/src/components/bottomSheetScrollable/ScrollableContainer.tsx @@ -0,0 +1,22 @@ +import React, { type FC, forwardRef } from 'react'; +import type { SimultaneousGesture } from 'react-native-gesture-handler'; +import { BottomSheetDraggableScrollable } from './BottomSheetDraggableScrollable'; + +interface ScrollableContainerProps { + nativeGesture?: SimultaneousGesture; + // biome-ignore lint/suspicious/noExplicitAny: 🤷‍♂️ + ScrollableComponent: FC; +} + +export const ScrollableContainer = forwardRef( + function ScrollableContainer( + { nativeGesture, ScrollableComponent, ...rest }, + ref + ) { + return ( + + + + ); + } +); diff --git a/src/components/bottomSheetScrollable/ScrollableContainer.web.tsx b/src/components/bottomSheetScrollable/ScrollableContainer.web.tsx new file mode 100644 index 000000000..7b56c9742 --- /dev/null +++ b/src/components/bottomSheetScrollable/ScrollableContainer.web.tsx @@ -0,0 +1,89 @@ +import React, { + type ComponentProps, + forwardRef, + useCallback, + useRef, +} from 'react'; +import type { LayoutChangeEvent, ViewProps } from 'react-native'; +import type { SimultaneousGesture } from 'react-native-gesture-handler'; +import Animated from 'react-native-reanimated'; +import { BottomSheetDraggableScrollable } from './BottomSheetDraggableScrollable'; + +interface ScrollableContainerProps { + nativeGesture: SimultaneousGesture; + setContentSize: (contentHeight: number) => void; + // biome-ignore lint/suspicious/noExplicitAny: 🤷‍♂️ + ScrollableComponent: any; + onLayout: ViewProps['onLayout']; +} + +/** + * Detect if the current browser is Safari or not. + */ +const isWebkit = () => { + // @ts-ignore + return navigator.userAgent.indexOf('Safari') > -1; +}; + +export const ScrollableContainer = forwardRef< + never, + ScrollableContainerProps & { animatedProps: never } +>(function ScrollableContainer( + { + nativeGesture, + ScrollableComponent, + animatedProps, + setContentSize, + onLayout, + ...rest + }, + ref +) { + //#region refs + const isInitialContentHeightCaptured = useRef(false); + //#endregion + + //#region callbacks + const renderScrollComponent = useCallback( + (props: ComponentProps) => ( + + ), + [animatedProps] + ); + + /** + * A workaround a bug in React Native Web [#1502](https://github.com/necolas/react-native-web/issues/1502), + * where the `onContentSizeChange` won't be call on initial render. + */ + const handleOnLayout = useCallback( + (event: LayoutChangeEvent) => { + if (onLayout) { + onLayout(event); + } + + if (!isInitialContentHeightCaptured.current) { + isInitialContentHeightCaptured.current = true; + if (!isWebkit()) { + return; + } + // @ts-ignore + window.requestAnimationFrame(() => { + // @ts-ignore + setContentSize(event.nativeEvent.target.clientHeight); + }); + } + }, + [onLayout, setContentSize] + ); + //#endregion + return ( + + + + ); +}); diff --git a/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx b/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx index 49237cb9d..8e7cffafb 100644 --- a/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx +++ b/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx @@ -1,28 +1,33 @@ -import React, { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'; -import { Platform } from 'react-native'; +import React, { + forwardRef, + useContext, + useImperativeHandle, + useMemo, +} from 'react'; +import { Gesture } from 'react-native-gesture-handler'; import { useAnimatedProps, useAnimatedStyle } from 'react-native-reanimated'; -import { NativeViewGestureHandler } from 'react-native-gesture-handler'; -import BottomSheetDraggableView from '../bottomSheetDraggableView'; -import BottomSheetRefreshControl from '../bottomSheetRefreshControl'; import { - useScrollHandler, - useScrollableSetter, - useBottomSheetInternal, -} from '../../hooks'; -import { - GESTURE_SOURCE, SCROLLABLE_DECELERATION_RATE_MAPPER, SCROLLABLE_STATE, - SCROLLABLE_TYPE, + type SCROLLABLE_TYPE, } from '../../constants'; -import { styles } from './styles'; +import { BottomSheetDraggableContext } from '../../contexts/gesture'; +import { + useBottomSheetInternal, + useScrollHandler, + useScrollableSetter, + useStableCallback, +} from '../../hooks'; +import { ScrollableContainer } from './ScrollableContainer'; +import { useBottomSheetContentSizeSetter } from './useBottomSheetContentSizeSetter'; export function createBottomSheetScrollableComponent( type: SCROLLABLE_TYPE, + // biome-ignore lint: to be addressed! ScrollableComponent: any ) { return forwardRef((props, ref) => { - // props + //#region props const { // hooks focusHook, @@ -37,43 +42,75 @@ export function createBottomSheetScrollableComponent( onRefresh, progressViewOffset, refreshControl, + scrollBuffer, + preserveScrollMomentum, + lockableScrollableContentOffsetY, // events onScroll, onScrollBeginDrag, onScrollEndDrag, + onContentSizeChange, ...rest + // biome-ignore lint: to be addressed! }: any = props; - - //#region refs - const nativeGestureRef = useRef(null); - const refreshControlGestureRef = useRef(null); //#endregion //#region hooks + const draggableGesture = useContext(BottomSheetDraggableContext); const { scrollableRef, scrollableContentOffsetY, scrollHandler } = useScrollHandler( scrollEventsHandlersHook, onScroll, onScrollBeginDrag, - onScrollEndDrag + onScrollEndDrag, + scrollBuffer, + preserveScrollMomentum, + lockableScrollableContentOffsetY ); const { - enableContentPanningGesture, animatedFooterHeight, animatedScrollableState, + enableContentPanningGesture, } = useBottomSheetInternal(); + const { setContentSize } = useBottomSheetContentSizeSetter(); //#endregion + if (!draggableGesture && enableContentPanningGesture) { + throw "'Scrollable' cannot be used out of the BottomSheet!"; + } + //#region variables const scrollableAnimatedProps = useAnimatedProps( () => ({ - decelerationRate: - SCROLLABLE_DECELERATION_RATE_MAPPER[animatedScrollableState.value], + ...(preserveScrollMomentum ? {} : {decelerationRate: SCROLLABLE_DECELERATION_RATE_MAPPER[animatedScrollableState.value]}), showsVerticalScrollIndicator: showsVerticalScrollIndicator ? animatedScrollableState.value === SCROLLABLE_STATE.UNLOCKED : showsVerticalScrollIndicator, }), - [showsVerticalScrollIndicator] + [animatedScrollableState, showsVerticalScrollIndicator] + ); + + const scrollableGesture = useMemo( + () => + draggableGesture + ? Gesture.Native() + // @ts-ignore + .simultaneousWithExternalGesture(draggableGesture) + .shouldCancelWhenOutside(false) + : undefined, + [draggableGesture] + ); + //#endregion + + //#region callbacks + const handleContentSizeChange = useStableCallback( + (contentWidth: number, contentHeight: number) => { + setContentSize(contentHeight); + + if (onContentSizeChange) { + onContentSizeChange(contentWidth, contentHeight); + } + } ); //#endregion @@ -84,7 +121,7 @@ export function createBottomSheetScrollableComponent( ? animatedFooterHeight.value : 0, }), - [enableFooterMarginAdjustment] + [animatedFooterHeight, enableFooterMarginAdjustment] ); const containerStyle = useMemo(() => { return enableFooterMarginAdjustment @@ -104,80 +141,32 @@ export function createBottomSheetScrollableComponent( type, scrollableContentOffsetY, onRefresh !== undefined, + scrollBuffer, + preserveScrollMomentum, focusHook ); //#endregion //#region render - if (Platform.OS === 'android') { - const scrollableContent = ( - - - - ); - return ( - - {onRefresh ? ( - - {scrollableContent} - - ) : ( - scrollableContent - )} - - ); - } return ( - - - - - + ); //#endregion }); diff --git a/src/components/bottomSheetScrollable/index.ts b/src/components/bottomSheetScrollable/index.ts index c2aad37a5..e07fcaa40 100644 --- a/src/components/bottomSheetScrollable/index.ts +++ b/src/components/bottomSheetScrollable/index.ts @@ -4,6 +4,8 @@ export { default as BottomSheetFlatList } from './BottomSheetFlatList'; export { default as BottomSheetScrollView } from './BottomSheetScrollView'; export { default as BottomSheetVirtualizedList } from './BottomSheetVirtualizedList'; +export { default as BottomSheetFlashList } from './BottomSheetFlashList'; + export type { BottomSheetFlatListMethods, BottomSheetScrollViewMethods, diff --git a/src/components/bottomSheetScrollable/types.d.ts b/src/components/bottomSheetScrollable/types.d.ts index 2822dbafc..a9ca5c602 100644 --- a/src/components/bottomSheetScrollable/types.d.ts +++ b/src/components/bottomSheetScrollable/types.d.ts @@ -6,18 +6,19 @@ import type { RefObject, } from 'react'; import type { - ScrollView, - VirtualizedListProps, - ScrollViewProps, FlatListProps, + NodeHandle, + ScrollResponderMixin, + ScrollViewComponent, + ScrollViewProps, SectionListProps, SectionListScrollParams, View, - ScrollViewComponent, - NodeHandle, + VirtualizedListProps, } from 'react-native'; import type Animated from 'react-native-reanimated'; import type { ScrollEventsHandlersHookType } from '../../types'; +import type { FlashListProps } from '@shopify/flash-list'; export interface BottomSheetScrollableProps { /** @@ -47,17 +48,33 @@ export interface BottomSheetScrollableProps { * @default useScrollEventsHandlersDefault */ scrollEventsHandlersHook?: ScrollEventsHandlersHookType; + + /** + * An initial scroll buffer to prevent the bottom sheet from immediately following the scroll gesture. + */ + scrollBuffer?: number; + + /** + * Whether or not to preserve scroll momentum when expanding a scrollable bottom sheet component. + */ + preserveScrollMomentum?: boolean; + + /** + * The optional lockable scrollable content offset ref, which will remain the same value when scrollable is locked. + */ + lockableScrollableContentOffsetY?: Animated.SharedValue; } export type ScrollableProps = | ScrollViewProps | FlatListProps + | FlashListProps | SectionListProps; //#region FlatList export type BottomSheetFlatListProps = Omit< Animated.AnimateProps>, - 'decelerationRate' | 'onScroll' | 'scrollEventThrottle' + 'decelerationRate' | 'scrollEventThrottle' > & BottomSheetScrollableProps & { ref?: Ref; @@ -81,6 +98,88 @@ export interface BottomSheetFlatListMethods { viewPosition?: number; }) => void; + /** + * Requires linear scan through data - use `scrollToIndex` instead if possible. + * May be janky without `getItemLayout` prop. + */ + scrollToItem: (params: { + animated?: boolean | null; + // biome-ignore lint: to be addressed! + item: any; + viewPosition?: number; + }) => void; + + /** + * Scroll to a specific content pixel offset, like a normal `ScrollView`. + */ + scrollToOffset: (params: { + animated?: boolean | null; + offset: number; + }) => void; + + /** + * Tells the list an interaction has occured, which should trigger viewability calculations, + * e.g. if waitForInteractions is true and the user has not scrolled. This is typically called + * by taps on items or by navigation actions. + */ + recordInteraction: () => void; + + /** + * Displays the scroll indicators momentarily. + */ + flashScrollIndicators: () => void; + + /** + * Provides a handle to the underlying scroll responder. + */ + getScrollResponder: () => ScrollResponderMixin | null | undefined; + + /** + * Provides a reference to the underlying host component + */ + getNativeScrollRef: () => + | RefObject + | RefObject + | null + | undefined; + + // biome-ignore lint: to be addressed! + getScrollableNode: () => any; + + // biome-ignore lint: to be addressed! + setNativeProps: (props: { [key: string]: any }) => void; +} +//#endregion + +//#region FlatList +export type BottomSheetFlashListProps = Omit< + FlashListProps, + | 'decelerationRate' + | 'scrollEventThrottle' + | 'renderScrollComponent' +> & + BottomSheetScrollableProps & { + ref?: Ref; + }; + +export interface BottomSheetFlashListMethods { + /** + * Scrolls to the end of the content. May be janky without `getItemLayout` prop. + */ + scrollToEnd: (params?: { animated?: boolean | null }) => void; + + /** + * Scrolls to the item at the specified index such that it is positioned in the viewable area + * such that viewPosition 0 places it at the top, 1 at the bottom, and 0.5 centered in the middle. + * Cannot scroll to locations outside the render window without specifying the getItemLayout prop. + */ + scrollToIndex: (params: { + animated?: boolean | null; + index: number; + viewOffset?: number; + viewPosition?: number; + }) => void; + /** * Requires linear scan through data - use `scrollToIndex` instead if possible. * May be janky without `getItemLayout` prop. @@ -114,7 +213,7 @@ export interface BottomSheetFlatListMethods { /** * Provides a handle to the underlying scroll responder. */ - getScrollResponder: () => ReactNode | null | undefined; + getScrollResponder: () => ScrollResponderMixin | null | undefined; /** * Provides a reference to the underlying host component @@ -127,6 +226,11 @@ export interface BottomSheetFlatListMethods { getScrollableNode: () => any; + /** + * Recalculates viewable items. + */ + updateViewableItems: () => void; + // TODO: use `unknown` instead of `any` for Typescript >= 3.0 setNativeProps: (props: { [key: string]: any }) => void; } @@ -175,11 +279,12 @@ export interface BottomSheetScrollViewMethods { * implement this method so that they can be composed while providing access * to the underlying scroll responder's methods. */ - getScrollResponder(): ReactNode; + getScrollResponder(): ScrollResponderMixin; + // biome-ignore lint: to be addressed! getScrollableNode(): any; - // Undocumented + // biome-ignore lint: to be addressed! getInnerViewNode(): any; /** @@ -198,7 +303,7 @@ export interface BottomSheetScrollViewMethods { //#endregion //#region SectionList -type BottomSheetSectionListProps = Omit< +export type BottomSheetSectionListProps = Omit< Animated.AnimateProps>, 'decelerationRate' | 'scrollEventThrottle' > & @@ -231,7 +336,7 @@ export interface BottomSheetSectionListMethods { /** * Provides a handle to the underlying scroll responder. */ - getScrollResponder(): ScrollView | undefined; + getScrollResponder(): ScrollResponderMixin | undefined; /** * Provides a handle to the underlying scroll node. @@ -259,6 +364,7 @@ export interface BottomSheetVirtualizedListMethods { }) => void; scrollToItem: (params: { animated?: boolean; + // biome-ignore lint: to be addressed! item: any; viewPosition?: number; }) => void; diff --git a/src/components/bottomSheetScrollable/useBottomSheetContentSizeSetter.ts b/src/components/bottomSheetScrollable/useBottomSheetContentSizeSetter.ts new file mode 100644 index 000000000..1d3380e3f --- /dev/null +++ b/src/components/bottomSheetScrollable/useBottomSheetContentSizeSetter.ts @@ -0,0 +1,29 @@ +import { useCallback } from 'react'; +import { useBottomSheetInternal } from '../../hooks'; + +/** + * A hook to set the content size properly into the bottom sheet, + * internals. + */ +export function useBottomSheetContentSizeSetter() { + //#region hooks + const { enableDynamicSizing, animatedContentHeight } = + useBottomSheetInternal(); + //#endregion + + //#region methods + const setContentSize = useCallback( + (contentHeight: number) => { + if (!enableDynamicSizing) { + return; + } + animatedContentHeight.value = contentHeight; + }, + [enableDynamicSizing, animatedContentHeight] + ); + //#endregion + + return { + setContentSize, + }; +} diff --git a/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx b/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx index eae09f7d4..aef8ab7f5 100644 --- a/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx +++ b/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx @@ -1,4 +1,8 @@ -import React, { memo, useCallback, forwardRef } from 'react'; +import React, { memo, useCallback, forwardRef, useEffect } from 'react'; +import type { + NativeSyntheticEvent, + TextInputFocusEventData, +} from 'react-native'; import { TextInput } from 'react-native-gesture-handler'; import { useBottomSheetInternal } from '../../hooks'; import type { BottomSheetTextInputProps } from './types'; @@ -13,7 +17,7 @@ const BottomSheetTextInputComponent = forwardRef< //#region callbacks const handleOnFocus = useCallback( - args => { + (args: NativeSyntheticEvent) => { shouldHandleKeyboardEvents.value = true; if (onFocus) { onFocus(args); @@ -22,7 +26,7 @@ const BottomSheetTextInputComponent = forwardRef< [onFocus, shouldHandleKeyboardEvents] ); const handleOnBlur = useCallback( - args => { + (args: NativeSyntheticEvent) => { shouldHandleKeyboardEvents.value = false; if (onBlur) { onBlur(args); @@ -32,6 +36,14 @@ const BottomSheetTextInputComponent = forwardRef< ); //#endregion + //#region effects + useEffect(() => { + return () => { + // Reset the flag on unmount + shouldHandleKeyboardEvents.value = false; + }; + }, [shouldHandleKeyboardEvents]); + //#endregion return ( { - const flattenStyle = StyleSheet.flatten(style); - const paddingBottom = - flattenStyle && 'paddingBottom' in flattenStyle - ? flattenStyle.paddingBottom - : 0; - return typeof paddingBottom === 'number' ? paddingBottom : 0; - }, [style]); - const containerAnimatedStyle = useAnimatedStyle( - () => ({ - paddingBottom: enableFooterMarginAdjustment - ? animatedFooterHeight.value + containerStylePaddingBottom - : containerStylePaddingBottom, - }), - [containerStylePaddingBottom, enableFooterMarginAdjustment] - ); - const containerStyle = useMemo( - () => [style, containerAnimatedStyle], - [style, containerAnimatedStyle] + //#region styles + const flattenStyle = useMemo( + () => StyleSheet.flatten(style), + [style] ); + const containerStyle = useAnimatedStyle(() => { + if (!enableFooterMarginAdjustment) { + return flattenStyle ?? {}; + } + + const marginBottom = + typeof flattenStyle?.marginBottom === 'number' + ? flattenStyle.marginBottom + : 0; - // callback + return { + ...(flattenStyle ?? {}), + marginBottom: marginBottom + animatedFooterHeight.value, + }; + }, [flattenStyle, enableFooterMarginAdjustment, animatedFooterHeight]); + //#endregion + + //#region callbacks const handleSettingScrollable = useCallback(() => { animatedScrollableContentOffsetY.value = 0; animatedScrollableType.value = SCROLLABLE_TYPE.VIEW; }, [animatedScrollableContentOffsetY, animatedScrollableType]); + const handleLayout = useCallback( + (event: LayoutChangeEvent) => { + if (enableDynamicSizing) { + animatedContentHeight.value = event.nativeEvent.layout.height; + } + + if (onLayout) { + onLayout(event); + } + + if (__DEV__) { + print({ + component: BottomSheetView.displayName, + method: 'handleLayout', + category: 'layout', + params: { + height: event.nativeEvent.layout.height, + }, + }); + } + }, + [onLayout, animatedContentHeight, enableDynamicSizing] + ); + //#endregion // effects useFocusHook(handleSettingScrollable); //render return ( - + {children} ); diff --git a/src/components/bottomSheetView/types.d.ts b/src/components/bottomSheetView/types.d.ts index 989a8f0f9..e2aa233cf 100644 --- a/src/components/bottomSheetView/types.d.ts +++ b/src/components/bottomSheetView/types.d.ts @@ -1,4 +1,4 @@ -import type { EffectCallback, DependencyList, ReactNode } from 'react'; +import type { DependencyList, EffectCallback, ReactNode } from 'react'; import type { ViewProps as RNViewProps } from 'react-native'; export interface BottomSheetViewProps extends RNViewProps { diff --git a/src/components/touchables/index.ts b/src/components/touchables/index.ts index 3a440747a..5e45ff6f9 100644 --- a/src/components/touchables/index.ts +++ b/src/components/touchables/index.ts @@ -1,19 +1,20 @@ import type { - TouchableOpacity as RNTouchableOpacity, TouchableHighlight as RNTouchableHighlight, + TouchableOpacity as RNTouchableOpacity, TouchableWithoutFeedback as RNTouchableWithoutFeedback, } from 'react-native'; import { - TouchableOpacity, TouchableHighlight, + TouchableOpacity, TouchableWithoutFeedback, // @ts-ignore } from './Touchables'; export default { - TouchableOpacity: TouchableOpacity as any as typeof RNTouchableOpacity, - TouchableHighlight: TouchableHighlight as any as typeof RNTouchableHighlight, + TouchableOpacity: TouchableOpacity as never as typeof RNTouchableOpacity, + TouchableHighlight: + TouchableHighlight as never as typeof RNTouchableHighlight, TouchableWithoutFeedback: - TouchableWithoutFeedback as any as typeof RNTouchableWithoutFeedback, + TouchableWithoutFeedback as never as typeof RNTouchableWithoutFeedback, }; diff --git a/src/constants.ts b/src/constants.ts index cc8fb9f71..87afaa18d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,65 +1,71 @@ import { Dimensions, Platform } from 'react-native'; -import Animated, { Easing } from 'react-native-reanimated'; +import type Animated from 'react-native-reanimated'; +import { Easing } from 'react-native-reanimated'; const { height: WINDOW_HEIGHT, width: WINDOW_WIDTH } = Dimensions.get('window'); const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get('screen'); enum GESTURE_SOURCE { UNDETERMINED = 0, - SCROLLABLE, - HANDLE, - CONTENT, + SCROLLABLE = 1, + HANDLE = 2, + CONTENT = 3, } enum SHEET_STATE { CLOSED = 0, - OPENED, - EXTENDED, - OVER_EXTENDED, - FILL_PARENT, + OPENED = 1, + EXTENDED = 2, + OVER_EXTENDED = 3, + FILL_PARENT = 4, } enum SCROLLABLE_STATE { LOCKED = 0, - UNLOCKED, - UNDETERMINED, + UNLOCKED = 1, + UNDETERMINED = 2, } enum SCROLLABLE_TYPE { UNDETERMINED = 0, - VIEW, - FLATLIST, - SCROLLVIEW, - SECTIONLIST, - VIRTUALIZEDLIST, + VIEW = 1, + FLATLIST = 2, + SCROLLVIEW = 3, + SECTIONLIST = 4, + VIRTUALIZEDLIST = 5, } enum ANIMATION_STATE { UNDETERMINED = 0, - RUNNING, - STOPPED, - INTERRUPTED, + RUNNING = 1, + STOPPED = 2, + INTERRUPTED = 3, } enum ANIMATION_SOURCE { NONE = 0, - MOUNT, - GESTURE, - USER, - CONTAINER_RESIZE, - SNAP_POINT_CHANGE, - KEYBOARD, + MOUNT = 1, + GESTURE = 2, + USER = 3, + CONTAINER_RESIZE = 4, + SNAP_POINT_CHANGE = 5, + KEYBOARD = 6, } enum ANIMATION_METHOD { - TIMING, - SPRING, + TIMING = 0, + SPRING = 1, } enum KEYBOARD_STATE { UNDETERMINED = 0, - SHOWN, - HIDDEN, + SHOWN = 1, + HIDDEN = 2, +} + +enum SNAP_POINT_TYPE { + PROVIDED = 0, + DYNAMIC = 1, } const ANIMATION_EASING: Animated.EasingFunction = Easing.out(Easing.exp); @@ -95,6 +101,7 @@ const SCROLLABLE_DECELERATION_RATE_MAPPER = { const MODAL_STACK_BEHAVIOR = { replace: 'replace', push: 'push', + switch: 'switch', }; const KEYBOARD_BEHAVIOR = { @@ -124,6 +131,7 @@ export { SCROLLABLE_TYPE, SCROLLABLE_STATE, KEYBOARD_STATE, + SNAP_POINT_TYPE, WINDOW_HEIGHT, WINDOW_WIDTH, SCREEN_HEIGHT, diff --git a/src/contexts/gesture.ts b/src/contexts/gesture.ts index 79ce72d30..a6b2d217a 100644 --- a/src/contexts/gesture.ts +++ b/src/contexts/gesture.ts @@ -1,11 +1,13 @@ import { createContext } from 'react'; -import type { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler'; +import type { Gesture } from 'react-native-gesture-handler/lib/typescript/handlers/gestures/gesture'; +import type { GestureHandlersHookType } from '../types'; export interface BottomSheetGestureHandlersContextType { - contentPanGestureHandler: (event: PanGestureHandlerGestureEvent) => void; - handlePanGestureHandler: (event: PanGestureHandlerGestureEvent) => void; - scrollablePanGestureHandler: (event: PanGestureHandlerGestureEvent) => void; + contentPanGestureHandler: ReturnType; + handlePanGestureHandler: ReturnType; } export const BottomSheetGestureHandlersContext = createContext(null); + +export const BottomSheetDraggableContext = createContext(null); diff --git a/src/contexts/internal.ts b/src/contexts/internal.ts index fd313de29..07fabb9b6 100644 --- a/src/contexts/internal.ts +++ b/src/contexts/internal.ts @@ -1,11 +1,9 @@ -import { createContext, RefObject } from 'react'; -import type { - PanGestureHandlerProps, - State, -} from 'react-native-gesture-handler'; -import type Animated from 'react-native-reanimated'; +import { type RefObject, createContext } from 'react'; +import type { State } from 'react-native-gesture-handler'; +import type { SharedValue } from 'react-native-reanimated'; import type { AnimateToPositionType, + BottomSheetGestureProps, BottomSheetProps, } from '../components/bottomSheet/types'; import type { @@ -18,51 +16,46 @@ import type { import type { Scrollable, ScrollableRef } from '../types'; export interface BottomSheetInternalContextType - extends Pick< - PanGestureHandlerProps, - | 'activeOffsetY' - | 'activeOffsetX' - | 'failOffsetY' - | 'failOffsetX' - | 'waitFor' - | 'simultaneousHandlers' - >, + extends Partial, Required< Pick< BottomSheetProps, | 'enableContentPanningGesture' | 'enableOverDrag' | 'enablePanDownToClose' + | 'enableDynamicSizing' | 'overDragResistanceFactor' > > { // animated states - animatedAnimationState: Animated.SharedValue; - animatedSheetState: Animated.SharedValue; - animatedScrollableState: Animated.SharedValue; - animatedKeyboardState: Animated.SharedValue; - animatedContentGestureState: Animated.SharedValue; - animatedHandleGestureState: Animated.SharedValue; + animatedAnimationState: SharedValue; + animatedSheetState: SharedValue; + animatedScrollableState: SharedValue; + animatedKeyboardState: SharedValue; + animatedContentGestureState: SharedValue; + animatedHandleGestureState: SharedValue; // animated values - animatedSnapPoints: Animated.SharedValue; - animatedPosition: Animated.SharedValue; - animatedIndex: Animated.SharedValue; - animatedContainerHeight: Animated.SharedValue; - animatedContentHeight: Animated.SharedValue; - animatedHighestSnapPoint: Animated.SharedValue; - animatedClosedPosition: Animated.SharedValue; - animatedFooterHeight: Animated.SharedValue; - animatedHandleHeight: Animated.SharedValue; - animatedKeyboardHeight: Animated.SharedValue; - animatedKeyboardHeightInContainer: Animated.SharedValue; - animatedScrollableType: Animated.SharedValue; - animatedScrollableContentOffsetY: Animated.SharedValue; - animatedScrollableOverrideState: Animated.SharedValue; - isScrollableRefreshable: Animated.SharedValue; - isContentHeightFixed: Animated.SharedValue; - isInTemporaryPosition: Animated.SharedValue; - shouldHandleKeyboardEvents: Animated.SharedValue; + animatedSnapPoints: SharedValue; + animatedPosition: SharedValue; + animatedIndex: SharedValue; + animatedContainerHeight: SharedValue; + animatedContentHeight: SharedValue; + animatedHighestSnapPoint: SharedValue; + animatedClosedPosition: SharedValue; + animatedFooterHeight: SharedValue; + animatedHandleHeight: SharedValue; + animatedKeyboardHeight: SharedValue; + animatedKeyboardHeightInContainer: SharedValue; + animatedScrollableType: SharedValue; + animatedScrollableContentOffsetY: SharedValue; + animatedScrollableOverrideState: SharedValue; + isScrollableLocked: SharedValue; + isScrollableRefreshable: SharedValue; + isScrollEnded: SharedValue; + isContentHeightFixed: SharedValue; + isInTemporaryPosition: SharedValue; + shouldHandleKeyboardEvents: SharedValue; // methods stopAnimation: () => void; diff --git a/src/contexts/modal/internal.ts b/src/contexts/modal/internal.ts index c6f73d605..b88a3bb89 100644 --- a/src/contexts/modal/internal.ts +++ b/src/contexts/modal/internal.ts @@ -1,15 +1,17 @@ -import { createContext, Ref } from 'react'; +import { type RefObject, createContext } from 'react'; import type { Insets } from 'react-native'; -import type Animated from 'react-native-reanimated'; -import type BottomSheet from '../../components/bottomSheet'; -import type { BottomSheetModalStackBehavior } from '../../components/bottomSheetModal'; +import type { SharedValue } from 'react-native-reanimated'; +import type { + BottomSheetModalPrivateMethods, + BottomSheetModalStackBehavior, +} from '../../components/bottomSheetModal'; export interface BottomSheetModalInternalContextType { - containerHeight: Animated.SharedValue; - containerOffset: Animated.SharedValue>; + containerHeight: SharedValue; + containerOffset: SharedValue>; mountSheet: ( key: string, - ref: Ref, + ref: RefObject, stackBehavior: BottomSheetModalStackBehavior ) => void; unmountSheet: (key: string) => void; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 52e9ec7fd..15c986b8b 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -18,7 +18,6 @@ export { useGestureEventsHandlersDefault } from './useGestureEventsHandlersDefau export { useKeyboard } from './useKeyboard'; export { useStableCallback } from './useStableCallback'; export { usePropsValidator } from './usePropsValidator'; -export { useNormalizedSnapPoints } from './useNormalizedSnapPoints'; +export { useAnimatedSnapPoints } from './useAnimatedSnapPoints'; export { useReactiveSharedValue } from './useReactiveSharedValue'; -export { useBottomSheetDynamicSnapPoints } from './useBottomSheetDynamicSnapPoints'; export { useBottomSheetGestureHandlers } from './useBottomSheetGestureHandlers'; diff --git a/src/hooks/useAnimatedSnapPoints.ts b/src/hooks/useAnimatedSnapPoints.ts new file mode 100644 index 000000000..debb14db5 --- /dev/null +++ b/src/hooks/useAnimatedSnapPoints.ts @@ -0,0 +1,136 @@ +import { + type SharedValue, + useDerivedValue, + useSharedValue, +} from 'react-native-reanimated'; +import type { BottomSheetProps } from '../components/bottomSheet'; +import { + INITIAL_CONTAINER_HEIGHT, + INITIAL_HANDLE_HEIGHT, + INITIAL_SNAP_POINT, +} from '../components/bottomSheet/constants'; +import { normalizeSnapPoint } from '../utilities'; + +/** + * Convert percentage snap points to pixels in screen and calculate + * the accurate snap points positions. + * @param snapPoints provided snap points. + * @param containerHeight BottomSheetContainer height. + * @param contentHeight content size. + * @param handleHeight handle size. + * @param footerHeight footer size. + * @param enableDynamicSizing + * @param maxDynamicContentSize + * @returns {SharedValue} + */ +export const useAnimatedSnapPoints = ( + snapPoints: BottomSheetProps['snapPoints'], + containerHeight: SharedValue, + contentHeight: SharedValue, + handleHeight: SharedValue, + footerHeight: SharedValue, + enableDynamicSizing: BottomSheetProps['enableDynamicSizing'], + maxDynamicContentSize: BottomSheetProps['maxDynamicContentSize'] +): [SharedValue, SharedValue, SharedValue] => { + const dynamicSnapPointIndex = useSharedValue(-1); + const normalizedSnapPoints = useDerivedValue(() => { + // early exit, if container layout is not ready + const isContainerLayoutReady = + containerHeight.value !== INITIAL_CONTAINER_HEIGHT; + if (!isContainerLayoutReady) { + return [INITIAL_SNAP_POINT]; + } + + // extract snap points from provided props + const _snapPoints = snapPoints + ? 'value' in snapPoints + ? snapPoints.value + : snapPoints + : []; + + // normalized all provided snap points, converting percentage + // values into absolute values. + let _normalizedSnapPoints = _snapPoints.map(snapPoint => + normalizeSnapPoint(snapPoint, containerHeight.value) + ) as number[]; + + // return normalized snap points if dynamic sizing is not enabled + if (!enableDynamicSizing) { + return _normalizedSnapPoints; + } + + // early exit, if handle height is not calculated yet. + if (handleHeight.value === INITIAL_HANDLE_HEIGHT) { + return [INITIAL_SNAP_POINT]; + } + + // early exit, if content height is not calculated yet. + if (contentHeight.value === INITIAL_CONTAINER_HEIGHT) { + return [INITIAL_SNAP_POINT]; + } + + // calculate a new snap point based on content height. + const dynamicSnapPoint = + containerHeight.value - + Math.min( + contentHeight.value + handleHeight.value + footerHeight.value, + maxDynamicContentSize !== undefined + ? maxDynamicContentSize + : containerHeight.value + ); + + // push dynamic snap point into the normalized snap points, + // only if it does not exists in the provided list already. + if (!_normalizedSnapPoints.includes(dynamicSnapPoint)) { + _normalizedSnapPoints.push(dynamicSnapPoint); + } + + // sort all snap points. + _normalizedSnapPoints = _normalizedSnapPoints.sort((a, b) => b - a); + + // locate the dynamic snap point index. + dynamicSnapPointIndex.value = + _normalizedSnapPoints.indexOf(dynamicSnapPoint); + + return _normalizedSnapPoints; + }, [ + snapPoints, + containerHeight, + handleHeight, + contentHeight, + footerHeight, + enableDynamicSizing, + maxDynamicContentSize, + dynamicSnapPointIndex, + ]); + + const hasDynamicSnapPoint = useDerivedValue(() => { + /** + * if dynamic sizing is enabled, then we return true. + */ + if (enableDynamicSizing) { + return true; + } + + // extract snap points from provided props + const _snapPoints = snapPoints + ? 'value' in snapPoints + ? snapPoints.value + : snapPoints + : []; + + /** + * if any of the snap points provided is a string, then we return true. + */ + if ( + _snapPoints.length && + _snapPoints.find(snapPoint => typeof snapPoint === 'string') + ) { + return true; + } + + return false; + }); + + return [normalizedSnapPoints, dynamicSnapPointIndex, hasDynamicSnapPoint]; +}; diff --git a/src/hooks/useBottomSheetDynamicSnapPoints.ts b/src/hooks/useBottomSheetDynamicSnapPoints.ts deleted file mode 100644 index a1c25735d..000000000 --- a/src/hooks/useBottomSheetDynamicSnapPoints.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useCallback } from 'react'; -import { useDerivedValue, useSharedValue } from 'react-native-reanimated'; -import { - INITIAL_HANDLE_HEIGHT, - INITIAL_SNAP_POINT, -} from '../components/bottomSheet/constants'; - -/** - * Provides dynamic content height calculating functionalities, by - * replacing the placeholder `CONTENT_HEIGHT` with calculated layout. - * @example - * [0, 'CONTENT_HEIGHT', '100%'] - * @param initialSnapPoints your snap point with content height placeholder. - * @returns { - * - animatedSnapPoints: an animated snap points to be set on `BottomSheet` or `BottomSheetModal`. - * - animatedHandleHeight: an animated handle height callback node to be set on `BottomSheet` or `BottomSheetModal`. - * - animatedContentHeight: an animated content height callback node to be set on `BottomSheet` or `BottomSheetModal`. - * - handleContentLayout: a `onLayout` callback method to be set on `BottomSheetView` component. - * } - */ -export const useBottomSheetDynamicSnapPoints = ( - initialSnapPoints: Array -) => { - // variables - const animatedContentHeight = useSharedValue(0); - const animatedHandleHeight = useSharedValue(INITIAL_HANDLE_HEIGHT); - const animatedSnapPoints = useDerivedValue(() => { - if ( - animatedHandleHeight.value === INITIAL_HANDLE_HEIGHT || - animatedContentHeight.value === 0 - ) { - return initialSnapPoints.map(() => INITIAL_SNAP_POINT); - } - const contentWithHandleHeight = - animatedContentHeight.value + animatedHandleHeight.value; - - return initialSnapPoints.map(snapPoint => - snapPoint === 'CONTENT_HEIGHT' ? contentWithHandleHeight : snapPoint - ); - }, []); - - // callbacks - const handleContentLayout = useCallback( - ({ - nativeEvent: { - layout: { height }, - }, - }) => { - animatedContentHeight.value = height; - }, - [animatedContentHeight] - ); - - return { - animatedSnapPoints, - animatedHandleHeight, - animatedContentHeight, - handleContentLayout, - }; -}; diff --git a/src/hooks/useBottomSheetInternal.ts b/src/hooks/useBottomSheetInternal.ts index 94c75fd74..72382e969 100644 --- a/src/hooks/useBottomSheetInternal.ts +++ b/src/hooks/useBottomSheetInternal.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; import { BottomSheetInternalContext, - BottomSheetInternalContextType, + type BottomSheetInternalContextType, } from '../contexts/internal'; export function useBottomSheetInternal( diff --git a/src/hooks/useBottomSheetModalInternal.ts b/src/hooks/useBottomSheetModalInternal.ts index 03fd5651d..b3c250e45 100644 --- a/src/hooks/useBottomSheetModalInternal.ts +++ b/src/hooks/useBottomSheetModalInternal.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; import { BottomSheetModalInternalContext, - BottomSheetModalInternalContextType, + type BottomSheetModalInternalContextType, } from '../contexts'; export function useBottomSheetModalInternal( diff --git a/src/hooks/useBottomSheetSpringConfigs.ts b/src/hooks/useBottomSheetSpringConfigs.ts index aef93b862..f379aa668 100644 --- a/src/hooks/useBottomSheetSpringConfigs.ts +++ b/src/hooks/useBottomSheetSpringConfigs.ts @@ -1,4 +1,3 @@ -import { useMemo } from 'react'; import type { WithSpringConfig } from 'react-native-reanimated'; /** @@ -8,5 +7,5 @@ import type { WithSpringConfig } from 'react-native-reanimated'; export const useBottomSheetSpringConfigs = ( configs: Omit ) => { - return useMemo(() => configs, [configs]); + return configs; }; diff --git a/src/hooks/useBottomSheetTimingConfigs.ts b/src/hooks/useBottomSheetTimingConfigs.ts index 9d2f61dc6..ff45b241f 100644 --- a/src/hooks/useBottomSheetTimingConfigs.ts +++ b/src/hooks/useBottomSheetTimingConfigs.ts @@ -1,21 +1,36 @@ import { useMemo } from 'react'; -import type { WithTimingConfig } from 'react-native-reanimated'; +import type { EasingFunction } from 'react-native'; +import type { + EasingFunctionFactory, + ReduceMotion, +} from 'react-native-reanimated'; import { ANIMATION_DURATION, ANIMATION_EASING } from '../constants'; +/** + * this is needed to avoid TS4023 + * https://github.com/microsoft/TypeScript/issues/5711 + */ +interface TimingConfig { + duration?: number; + easing?: EasingFunction | EasingFunctionFactory; + reduceMotion?: ReduceMotion; +} + /** * Generate timing animation configs. * @default * - easing: Easing.out(Easing.exp) - * - duration 250 + * - duration: 250 * @param configs overridable configs. */ -export const useBottomSheetTimingConfigs = (configs: WithTimingConfig) => { +export const useBottomSheetTimingConfigs = (configs: TimingConfig) => { return useMemo(() => { - const _configs: WithTimingConfig = { + const _configs: TimingConfig = { easing: configs.easing || ANIMATION_EASING, duration: configs.duration || ANIMATION_DURATION, + reduceMotion: configs.reduceMotion, }; return _configs; - }, [configs.duration, configs.easing]); + }, [configs.duration, configs.easing, configs.reduceMotion]); }; diff --git a/src/hooks/useGestureEventsHandlersDefault.tsx b/src/hooks/useGestureEventsHandlersDefault.tsx index b44bfa510..50c00a48b 100644 --- a/src/hooks/useGestureEventsHandlersDefault.tsx +++ b/src/hooks/useGestureEventsHandlersDefault.tsx @@ -1,6 +1,9 @@ import { Keyboard, Platform } from 'react-native'; -import { runOnJS, useWorkletCallback } from 'react-native-reanimated'; -import { useBottomSheetInternal } from './useBottomSheetInternal'; +import { + runOnJS, + useSharedValue, + useWorkletCallback, +} from 'react-native-reanimated'; import { ANIMATION_SOURCE, GESTURE_SOURCE, @@ -9,11 +12,12 @@ import { WINDOW_HEIGHT, } from '../constants'; import type { - GestureEventsHandlersHookType, GestureEventHandlerCallbackType, + GestureEventsHandlersHookType, } from '../types'; import { clamp } from '../utilities/clamp'; import { snapPoint } from '../utilities/snapPoint'; +import { useBottomSheetInternal } from './useBottomSheetInternal'; type GestureEventContextType = { initialPosition: number; @@ -21,6 +25,22 @@ type GestureEventContextType = { isScrollablePositionLocked: boolean; }; +const INITIAL_CONTEXT: GestureEventContextType = { + initialPosition: 0, + initialKeyboardState: KEYBOARD_STATE.UNDETERMINED, + isScrollablePositionLocked: false, +}; + +const dismissKeyboard = Keyboard.dismiss; + +// biome-ignore lint: to be addressed! +const resetContext = (context: any) => { + 'worklet'; + Object.keys(context).map(key => { + context[key] = undefined; + }); +}; + export const useGestureEventsHandlersDefault: GestureEventsHandlersHookType = () => { //#region variables @@ -39,335 +59,364 @@ export const useGestureEventsHandlersDefault: GestureEventsHandlersHookType = overDragResistanceFactor, isInTemporaryPosition, isScrollableRefreshable, + isScrollableLocked, animateToPosition, stopAnimation, } = useBottomSheetInternal(); + + const context = useSharedValue({ + ...INITIAL_CONTEXT, + }); //#endregion //#region gesture methods - const handleOnStart: GestureEventHandlerCallbackType = - useWorkletCallback( - function handleOnStart(__, _, context) { - // cancel current animation - stopAnimation(); + const handleOnStart: GestureEventHandlerCallbackType = useWorkletCallback( + function handleOnStart(__, _) { + // cancel current animation + stopAnimation(); - // store current animated position - context.initialPosition = animatedPosition.value; - context.initialKeyboardState = animatedKeyboardState.value; + // store current animated position + context.value = { + ...context.value, + initialPosition: animatedPosition.value, + initialKeyboardState: animatedKeyboardState.value, + }; - /** - * if the scrollable content is scrolled, then - * we lock the position. - */ - if (animatedScrollableContentOffsetY.value > 0) { - context.isScrollablePositionLocked = true; - } - }, - [ - stopAnimation, - animatedPosition, - animatedKeyboardState, - animatedScrollableContentOffsetY, - ] - ); - const handleOnActive: GestureEventHandlerCallbackType = - useWorkletCallback( - function handleOnActive(source, { translationY }, context) { - let highestSnapPoint = animatedHighestSnapPoint.value; + /** + * if the scrollable content is scrolled, then + * we lock the position. + */ + if (animatedScrollableContentOffsetY.value > 0) { + context.value = { + ...context.value, + isScrollablePositionLocked: true, + }; + } + }, + [ + stopAnimation, + animatedPosition, + animatedKeyboardState, + animatedScrollableContentOffsetY, + ] + ); + const handleOnChange: GestureEventHandlerCallbackType = useWorkletCallback( + function handleOnChange(source, { translationY }) { + let highestSnapPoint = animatedHighestSnapPoint.value; - /** - * if keyboard is shown, then we set the highest point to the current - * position which includes the keyboard height. - */ - if ( - isInTemporaryPosition.value && - context.initialKeyboardState === KEYBOARD_STATE.SHOWN - ) { - highestSnapPoint = context.initialPosition; - } + /** + * if keyboard is shown, then we set the highest point to the current + * position which includes the keyboard height. + */ + if ( + isInTemporaryPosition.value && + context.value.initialKeyboardState === KEYBOARD_STATE.SHOWN + ) { + highestSnapPoint = context.value.initialPosition; + } - /** - * if current position is out of provided `snapPoints` and smaller then - * highest snap pont, then we set the highest point to the current position. - */ - if ( - isInTemporaryPosition.value && - context.initialPosition < highestSnapPoint - ) { - highestSnapPoint = context.initialPosition; - } + /** + * if current position is out of provided `snapPoints` and smaller then + * highest snap pont, then we set the highest point to the current position. + */ + if ( + isInTemporaryPosition.value && + context.value.initialPosition < highestSnapPoint + ) { + highestSnapPoint = context.value.initialPosition; + } - const lowestSnapPoint = enablePanDownToClose - ? animatedContainerHeight.value - : animatedSnapPoints.value[0]; + const lowestSnapPoint = enablePanDownToClose + ? animatedContainerHeight.value + : animatedSnapPoints.value[0]; - /** - * if scrollable is refreshable and sheet position at the highest - * point, then do not interact with current gesture. - */ - if ( - source === GESTURE_SOURCE.SCROLLABLE && - isScrollableRefreshable.value && - animatedPosition.value === highestSnapPoint - ) { - return; - } + /** + * if scrollable is refreshable and sheet position at the highest + * point, then do not interact with current gesture. + */ + if ( + source === GESTURE_SOURCE.CONTENT && + isScrollableRefreshable.value && + animatedPosition.value === highestSnapPoint + ) { + return; + } - /** - * a negative scrollable content offset to be subtracted from accumulated - * current position and gesture translation Y to allow user to drag the sheet, - * when scrollable position at the top. - * a negative scrollable content offset when the scrollable is not locked. - */ - const negativeScrollableContentOffset = - (context.initialPosition === highestSnapPoint && - source === GESTURE_SOURCE.SCROLLABLE) || - !context.isScrollablePositionLocked - ? animatedScrollableContentOffsetY.value * -1 - : 0; + /** + * if scrollable isn't currently marked as translatable, then do not + * interact with current gesture. + */ + if (source === GESTURE_SOURCE.SCROLLABLE && !isScrollableLocked.value) { + return; + } - /** - * an accumulated value of starting position with gesture translation y. - */ - const draggedPosition = context.initialPosition + translationY; + /** + * a negative scrollable content offset to be subtracted from accumulated + * current position and gesture translation Y to allow user to drag the sheet, + * when scrollable position at the top. + * a negative scrollable content offset when the scrollable is not locked. + */ + const negativeScrollableContentOffset = + (context.value.initialPosition === highestSnapPoint && + source === GESTURE_SOURCE.CONTENT) || + !context.value.isScrollablePositionLocked + ? animatedScrollableContentOffsetY.value * -1 + : 0; - /** - * an accumulated value of dragged position and negative scrollable content offset, - * this will insure locking sheet position when user is scrolling the scrollable until, - * they reach to the top of the scrollable. - */ - const accumulatedDraggedPosition = - draggedPosition + negativeScrollableContentOffset; + /** + * an accumulated value of starting position with gesture translation y. + */ + const draggedPosition = context.value.initialPosition + translationY; - /** - * a clamped value of the accumulated dragged position, to insure keeping the dragged - * position between the highest and lowest snap points. - */ - const clampedPosition = clamp( - accumulatedDraggedPosition, - highestSnapPoint, - lowestSnapPoint - ); + /** + * an accumulated value of dragged position and negative scrollable content offset, + * this will insure locking sheet position when user is scrolling the scrollable until, + * they reach to the top of the scrollable. + */ + const accumulatedDraggedPosition = + draggedPosition + negativeScrollableContentOffset; - /** - * if scrollable position is locked and the animated position - * reaches the highest point, then we unlock the scrollable position. - */ - if ( - context.isScrollablePositionLocked && - source === GESTURE_SOURCE.SCROLLABLE && - animatedPosition.value === highestSnapPoint - ) { - context.isScrollablePositionLocked = false; - } - - /** - * over-drag implementation. - */ - if (enableOverDrag) { - if ( - (source === GESTURE_SOURCE.HANDLE || - animatedScrollableType.value === SCROLLABLE_TYPE.VIEW) && - draggedPosition < highestSnapPoint - ) { - const resistedPosition = - highestSnapPoint - - Math.sqrt(1 + (highestSnapPoint - draggedPosition)) * - overDragResistanceFactor; - animatedPosition.value = resistedPosition; - return; - } - - if ( - source === GESTURE_SOURCE.HANDLE && - draggedPosition > lowestSnapPoint - ) { - const resistedPosition = - lowestSnapPoint + - Math.sqrt(1 + (draggedPosition - lowestSnapPoint)) * - overDragResistanceFactor; - animatedPosition.value = resistedPosition; - return; - } - - if ( - source === GESTURE_SOURCE.SCROLLABLE && - draggedPosition + negativeScrollableContentOffset > - lowestSnapPoint - ) { - const resistedPosition = - lowestSnapPoint + - Math.sqrt( - 1 + - (draggedPosition + - negativeScrollableContentOffset - - lowestSnapPoint) - ) * - overDragResistanceFactor; - animatedPosition.value = resistedPosition; - return; - } - } + /** + * a clamped value of the accumulated dragged position, to insure keeping the dragged + * position between the highest and lowest snap points. + */ + const clampedPosition = clamp( + accumulatedDraggedPosition, + highestSnapPoint, + lowestSnapPoint + ); - animatedPosition.value = clampedPosition; - }, - [ - enableOverDrag, - enablePanDownToClose, - overDragResistanceFactor, - isInTemporaryPosition, - isScrollableRefreshable, - animatedHighestSnapPoint, - animatedContainerHeight, - animatedSnapPoints, - animatedPosition, - animatedScrollableType, - animatedScrollableContentOffsetY, - ] - ); - const handleOnEnd: GestureEventHandlerCallbackType = - useWorkletCallback( - function handleOnEnd( - source, - { translationY, absoluteY, velocityY }, - context + /** + * if scrollable position is locked and the animated position + * reaches the highest point, then we unlock the scrollable position. + */ + if ( + context.value.isScrollablePositionLocked && + source === GESTURE_SOURCE.CONTENT && + animatedPosition.value === highestSnapPoint ) { - const highestSnapPoint = animatedHighestSnapPoint.value; - const isSheetAtHighestSnapPoint = - animatedPosition.value === highestSnapPoint; + context.value = { + ...context.value, + isScrollablePositionLocked: false, + }; + } - /** - * if scrollable is refreshable and sheet position at the highest - * point, then do not interact with current gesture. - */ + /** + * over-drag implementation. + */ + if (enableOverDrag) { if ( - source === GESTURE_SOURCE.SCROLLABLE && - isScrollableRefreshable.value && - isSheetAtHighestSnapPoint + (source === GESTURE_SOURCE.HANDLE || + animatedScrollableType.value === SCROLLABLE_TYPE.VIEW) && + draggedPosition < highestSnapPoint ) { + const resistedPosition = + highestSnapPoint - + Math.sqrt(1 + (highestSnapPoint - draggedPosition)) * + overDragResistanceFactor; + animatedPosition.value = resistedPosition; return; } - /** - * if the sheet is in a temporary position and the gesture ended above - * the current position, then we snap back to the temporary position. - */ if ( - isInTemporaryPosition.value && - context.initialPosition >= animatedPosition.value + source === GESTURE_SOURCE.HANDLE && + draggedPosition > lowestSnapPoint ) { - if (context.initialPosition > animatedPosition.value) { - animateToPosition( - context.initialPosition, - ANIMATION_SOURCE.GESTURE, - velocityY / 2 - ); - } + const resistedPosition = + lowestSnapPoint + + Math.sqrt(1 + (draggedPosition - lowestSnapPoint)) * + overDragResistanceFactor; + animatedPosition.value = resistedPosition; return; } - /** - * close keyboard if current position is below the recorded - * start position and keyboard still shown. - */ - const isScrollable = - animatedScrollableType.value !== SCROLLABLE_TYPE.UNDETERMINED && - animatedScrollableType.value !== SCROLLABLE_TYPE.VIEW; - - /** - * if keyboard is shown and the sheet is dragged down, - * then we dismiss the keyboard. - */ if ( - context.initialKeyboardState === KEYBOARD_STATE.SHOWN && - animatedPosition.value > context.initialPosition + source === GESTURE_SOURCE.CONTENT && + draggedPosition + negativeScrollableContentOffset > lowestSnapPoint ) { - /** - * if the platform is ios, current content is scrollable and - * the end touch point is below the keyboard position then - * we exit the method. - * - * because the the keyboard dismiss is interactive in iOS. - */ - if ( - !( - Platform.OS === 'ios' && - isScrollable && - absoluteY > WINDOW_HEIGHT - animatedKeyboardHeight.value - ) - ) { - runOnJS(Keyboard.dismiss)(); - } + const resistedPosition = + lowestSnapPoint + + Math.sqrt( + 1 + + (draggedPosition + + negativeScrollableContentOffset - + lowestSnapPoint) + ) * + overDragResistanceFactor; + animatedPosition.value = resistedPosition; + return; } + } - /** - * reset isInTemporaryPosition value - */ - if (isInTemporaryPosition.value) { - isInTemporaryPosition.value = false; - } + animatedPosition.value = clampedPosition; + }, + [ + enableOverDrag, + enablePanDownToClose, + overDragResistanceFactor, + isInTemporaryPosition, + isScrollableRefreshable, + animatedHighestSnapPoint, + animatedContainerHeight, + animatedSnapPoints, + animatedPosition, + animatedScrollableType, + animatedScrollableContentOffsetY, + ] + ); + const handleOnEnd: GestureEventHandlerCallbackType = useWorkletCallback( + function handleOnEnd(source, { translationY, absoluteY, velocityY }) { + const highestSnapPoint = animatedHighestSnapPoint.value; - /** - * clone snap points array, and insert the container height - * if pan down to close is enabled. - */ - const snapPoints = animatedSnapPoints.value.slice(); - if (enablePanDownToClose) { - snapPoints.unshift(animatedClosedPosition.value); - } + const isSheetAtHighestSnapPoint = + animatedPosition.value === highestSnapPoint; - /** - * calculate the destination point, using redash. - */ - const destinationPoint = snapPoint( - translationY + context.initialPosition, - velocityY, - snapPoints - ); + /** + * if scrollable is refreshable and sheet position at the highest + * point, then do not interact with current gesture. + */ + if ( + source === GESTURE_SOURCE.CONTENT && + isScrollableRefreshable.value && + isSheetAtHighestSnapPoint + ) { + return; + } - /** - * if destination point is the same as the current position, - * then no need to perform animation. - */ - if (destinationPoint === animatedPosition.value) { - return; + /** + * if the sheet is in a temporary position and the gesture ended above + * the current position, then we snap back to the temporary position. + */ + if ( + isInTemporaryPosition.value && + context.value.initialPosition >= animatedPosition.value + ) { + if (context.value.initialPosition > animatedPosition.value) { + animateToPosition( + context.value.initialPosition, + ANIMATION_SOURCE.GESTURE, + velocityY / 2 + ); } + return; + } + + if (!isScrollableLocked.value && animatedSnapPoints.value.includes(animatedPosition.value)) { + return; + } + + /** + * close keyboard if current position is below the recorded + * start position and keyboard still shown. + */ + const isScrollable = + animatedScrollableType.value !== SCROLLABLE_TYPE.UNDETERMINED && + animatedScrollableType.value !== SCROLLABLE_TYPE.VIEW; - const wasGestureHandledByScrollView = - source === GESTURE_SOURCE.SCROLLABLE && - animatedScrollableContentOffsetY.value > 0; + /** + * if keyboard is shown and the sheet is dragged down, + * then we dismiss the keyboard. + */ + if ( + context.value.initialKeyboardState === KEYBOARD_STATE.SHOWN && + animatedPosition.value > context.value.initialPosition + ) { /** - * prevents snapping from top to middle / bottom with repeated interrupted scrolls + * if the platform is ios, current content is scrollable and + * the end touch point is below the keyboard position then + * we exit the method. + * + * because the the keyboard dismiss is interactive in iOS. */ - if (wasGestureHandledByScrollView && isSheetAtHighestSnapPoint) { - return; + if ( + !( + Platform.OS === 'ios' && + isScrollable && + absoluteY > WINDOW_HEIGHT - animatedKeyboardHeight.value + ) + ) { + runOnJS(dismissKeyboard)(); } + } + + /** + * reset isInTemporaryPosition value + */ + if (isInTemporaryPosition.value) { + isInTemporaryPosition.value = false; + } + + /** + * clone snap points array, and insert the container height + * if pan down to close is enabled. + */ + const snapPoints = animatedSnapPoints.value.slice(); + if (enablePanDownToClose) { + snapPoints.unshift(animatedClosedPosition.value); + } - animateToPosition( - destinationPoint, - ANIMATION_SOURCE.GESTURE, - velocityY / 2 - ); + /** + * calculate the destination point, using redash. + */ + const destinationPoint = snapPoint( + translationY + context.value.initialPosition, + velocityY, + snapPoints + ); + + /** + * if destination point is the same as the current position, + * then no need to perform animation. + */ + if (destinationPoint === animatedPosition.value) { + return; + } + + const wasGestureHandledByScrollView = + source === GESTURE_SOURCE.CONTENT && + animatedScrollableContentOffsetY.value > 0; + /** + * prevents snapping from top to middle / bottom with repeated interrupted scrolls + */ + if (wasGestureHandledByScrollView && isSheetAtHighestSnapPoint) { + return; + } + + animateToPosition( + destinationPoint, + ANIMATION_SOURCE.GESTURE, + velocityY / 2 + ); + }, + [ + enablePanDownToClose, + isInTemporaryPosition, + isScrollableLocked, + isScrollableRefreshable, + animatedClosedPosition, + animatedHighestSnapPoint, + animatedKeyboardHeight, + animatedPosition, + animatedScrollableType, + animatedSnapPoints, + animatedScrollableContentOffsetY, + animateToPosition, + ] + ); + + const handleOnFinalize: GestureEventHandlerCallbackType = + useWorkletCallback( + function handleOnFinalize() { + resetContext(context); }, - [ - enablePanDownToClose, - isInTemporaryPosition, - isScrollableRefreshable, - animatedClosedPosition, - animatedHighestSnapPoint, - animatedKeyboardHeight, - animatedPosition, - animatedScrollableType, - animatedSnapPoints, - animatedScrollableContentOffsetY, - animateToPosition, - ] + [context] ); //#endregion return { handleOnStart, - handleOnActive, + handleOnChange, handleOnEnd, + handleOnFinalize, }; }; diff --git a/example/app/src/screens/advanced/customGestureHandling/useCustomGestureEventsHandlers.ts b/src/hooks/useGestureEventsHandlersDefault.web.tsx similarity index 63% rename from example/app/src/screens/advanced/customGestureHandling/useCustomGestureEventsHandlers.ts rename to src/hooks/useGestureEventsHandlersDefault.web.tsx index c9b80c4a5..b874ab547 100644 --- a/example/app/src/screens/advanced/customGestureHandling/useCustomGestureEventsHandlers.ts +++ b/src/hooks/useGestureEventsHandlersDefault.web.tsx @@ -1,20 +1,47 @@ import { Keyboard, Platform } from 'react-native'; -import { runOnJS, useWorkletCallback } from 'react-native-reanimated'; -import { clamp, snapPoint } from 'react-native-redash'; import { - useBottomSheetInternal, + runOnJS, + useSharedValue, + useWorkletCallback, +} from 'react-native-reanimated'; +import { + ANIMATION_SOURCE, GESTURE_SOURCE, KEYBOARD_STATE, SCROLLABLE_TYPE, WINDOW_HEIGHT, - GestureEventHandlerCallbackType, - ANIMATION_SOURCE, -} from '@gorhom/bottom-sheet'; -import { useGestureTranslationY } from './GestureTranslationContext'; +} from '../constants'; +import type { GestureEventHandlerCallbackType } from '../types'; +import { clamp } from '../utilities/clamp'; +import { snapPoint } from '../utilities/snapPoint'; +import { useBottomSheetInternal } from './useBottomSheetInternal'; + +type GestureEventContextType = { + initialPosition: number; + initialKeyboardState: KEYBOARD_STATE; + initialTranslationY: number; + isScrollablePositionLocked: boolean; +}; + +const INITIAL_CONTEXT: GestureEventContextType = { + initialPosition: 0, + initialTranslationY: 0, + initialKeyboardState: KEYBOARD_STATE.UNDETERMINED, + isScrollablePositionLocked: false, +}; + +const dismissKeyboardOnJs = runOnJS(Keyboard.dismiss); -export const useCustomGestureEventsHandlers = () => { - // hooks - const gestureTranslationY = useGestureTranslationY(); +// biome-ignore lint: to be addressed! +const resetContext = (context: any) => { + 'worklet'; + Object.keys(context).map(key => { + context[key] = undefined; + }); +}; + +export const useGestureEventsHandlersDefault = () => { + //#region variables const { animatedPosition, animatedSnapPoints, @@ -34,42 +61,54 @@ export const useCustomGestureEventsHandlers = () => { stopAnimation, } = useBottomSheetInternal(); + const context = useSharedValue({ + ...INITIAL_CONTEXT, + }); + //#endregion + //#region gesture methods const handleOnStart: GestureEventHandlerCallbackType = useWorkletCallback( - function handleOnStart(_, { translationY }, context) { + function handleOnStart(__, { translationY }) { // cancel current animation stopAnimation(); // store current animated position - context.initialPosition = animatedPosition.value; - context.initialKeyboardState = animatedKeyboardState.value; + context.value = { + ...context.value, + initialPosition: animatedPosition.value, + initialKeyboardState: animatedKeyboardState.value, + initialTranslationY: translationY, + }; /** * if the scrollable content is scrolled, then * we lock the position. */ if (animatedScrollableContentOffsetY.value > 0) { - context.isScrollablePositionLocked = true; + context.value.isScrollablePositionLocked = true; } - gestureTranslationY.value = translationY; }, - [animatedPosition, animatedKeyboardState, animatedScrollableContentOffsetY] + [ + stopAnimation, + animatedPosition, + animatedKeyboardState, + animatedScrollableContentOffsetY, + ] ); - const handleOnActive: GestureEventHandlerCallbackType = useWorkletCallback( - function handleOnActive(source, { translationY }, context) { - gestureTranslationY.value = translationY; + const handleOnChange: GestureEventHandlerCallbackType = useWorkletCallback( + function handleOnChange(source, { translationY }) { + let highestSnapPoint = animatedHighestSnapPoint.value; - let highestSnapPoint = - animatedSnapPoints.value[animatedSnapPoints.value.length - 1]; + translationY = translationY - context.value.initialTranslationY; /** * if keyboard is shown, then we set the highest point to the current * position which includes the keyboard height. */ if ( isInTemporaryPosition.value && - context.initialKeyboardState === KEYBOARD_STATE.SHOWN + context.value.initialKeyboardState === KEYBOARD_STATE.SHOWN ) { - highestSnapPoint = context.initialPosition; + highestSnapPoint = context.value.initialPosition; } /** @@ -78,9 +117,9 @@ export const useCustomGestureEventsHandlers = () => { */ if ( isInTemporaryPosition.value && - context.initialPosition < highestSnapPoint + context.value.initialPosition < highestSnapPoint ) { - highestSnapPoint = context.initialPosition; + highestSnapPoint = context.value.initialPosition; } const lowestSnapPoint = enablePanDownToClose @@ -92,7 +131,7 @@ export const useCustomGestureEventsHandlers = () => { * point, then do not interact with current gesture. */ if ( - source === GESTURE_SOURCE.SCROLLABLE && + source === GESTURE_SOURCE.CONTENT && isScrollableRefreshable.value && animatedPosition.value === highestSnapPoint ) { @@ -106,16 +145,16 @@ export const useCustomGestureEventsHandlers = () => { * a negative scrollable content offset when the scrollable is not locked. */ const negativeScrollableContentOffset = - (context.initialPosition === highestSnapPoint && - source === GESTURE_SOURCE.SCROLLABLE) || - !context.isScrollablePositionLocked + (context.value.initialPosition === highestSnapPoint && + source === GESTURE_SOURCE.CONTENT) || + !context.value.isScrollablePositionLocked ? animatedScrollableContentOffsetY.value * -1 : 0; /** * an accumulated value of starting position with gesture translation y. */ - const draggedPosition = context.initialPosition + translationY; + const draggedPosition = context.value.initialPosition + translationY; /** * an accumulated value of dragged position and negative scrollable content offset, @@ -127,41 +166,24 @@ export const useCustomGestureEventsHandlers = () => { /** * a clamped value of the accumulated dragged position, to insure keeping the dragged - * position between the highest and middle snap points. + * position between the highest and lowest snap points. */ - const secondHighestSnapPoint = - animatedSnapPoints.value[animatedSnapPoints.value.length - 2]; - const isDraggingFromBottom = - context.initialPosition > secondHighestSnapPoint; - - const clampedPosition = (() => { - if (source === GESTURE_SOURCE.SCROLLABLE) { - const clampSource = (() => { - if (isDraggingFromBottom) { - return accumulatedDraggedPosition; - } - return Math.min(draggedPosition, secondHighestSnapPoint); - })(); - return clamp(clampSource, highestSnapPoint, lowestSnapPoint); - } else { - return clamp( - accumulatedDraggedPosition, - highestSnapPoint, - lowestSnapPoint - ); - } - })(); + const clampedPosition = clamp( + accumulatedDraggedPosition, + highestSnapPoint, + lowestSnapPoint + ); /** * if scrollable position is locked and the animated position * reaches the highest point, then we unlock the scrollable position. */ if ( - context.isScrollablePositionLocked && - source === GESTURE_SOURCE.SCROLLABLE && + context.value.isScrollablePositionLocked && + source === GESTURE_SOURCE.CONTENT && animatedPosition.value === highestSnapPoint ) { - context.isScrollablePositionLocked = false; + context.value.isScrollablePositionLocked = false; } /** @@ -192,6 +214,23 @@ export const useCustomGestureEventsHandlers = () => { animatedPosition.value = resistedPosition; return; } + + if ( + source === GESTURE_SOURCE.CONTENT && + draggedPosition + negativeScrollableContentOffset > lowestSnapPoint + ) { + const resistedPosition = + lowestSnapPoint + + Math.sqrt( + 1 + + (draggedPosition + + negativeScrollableContentOffset - + lowestSnapPoint) + ) * + overDragResistanceFactor; + animatedPosition.value = resistedPosition; + return; + } } animatedPosition.value = clampedPosition; @@ -200,35 +239,45 @@ export const useCustomGestureEventsHandlers = () => { enableOverDrag, enablePanDownToClose, overDragResistanceFactor, - animatedContainerHeight, - animatedKeyboardState, - animatedPosition, - animatedSnapPoints, isInTemporaryPosition, isScrollableRefreshable, + animatedHighestSnapPoint, + animatedContainerHeight, + animatedSnapPoints, + animatedPosition, + animatedScrollableType, animatedScrollableContentOffsetY, ] ); const handleOnEnd: GestureEventHandlerCallbackType = useWorkletCallback( - function handleOnEnd( - source, - { translationY, absoluteY, velocityY }, - context - ) { + function handleOnEnd(source, { translationY, absoluteY, velocityY }) { const highestSnapPoint = animatedHighestSnapPoint.value; const isSheetAtHighestSnapPoint = animatedPosition.value === highestSnapPoint; + + /** + * if scrollable is refreshable and sheet position at the highest + * point, then do not interact with current gesture. + */ + if ( + source === GESTURE_SOURCE.CONTENT && + isScrollableRefreshable.value && + isSheetAtHighestSnapPoint + ) { + return; + } + /** * if the sheet is in a temporary position and the gesture ended above * the current position, then we snap back to the temporary position. */ if ( isInTemporaryPosition.value && - context.initialPosition >= animatedPosition.value + context.value.initialPosition >= animatedPosition.value ) { - if (context.initialPosition > animatedPosition.value) { + if (context.value.initialPosition > animatedPosition.value) { animateToPosition( - context.initialPosition, + context.value.initialPosition, ANIMATION_SOURCE.GESTURE, velocityY / 2 ); @@ -249,8 +298,8 @@ export const useCustomGestureEventsHandlers = () => { * then we dismiss the keyboard. */ if ( - context.initialKeyboardState === KEYBOARD_STATE.SHOWN && - animatedPosition.value > context.initialPosition + context.value.initialKeyboardState === KEYBOARD_STATE.SHOWN && + animatedPosition.value > context.value.initialPosition ) { /** * if the platform is ios, current content is scrollable and @@ -266,7 +315,7 @@ export const useCustomGestureEventsHandlers = () => { absoluteY > WINDOW_HEIGHT - animatedKeyboardHeight.value ) ) { - runOnJS(Keyboard.dismiss)(); + dismissKeyboardOnJs(); } } @@ -289,23 +338,11 @@ export const useCustomGestureEventsHandlers = () => { /** * calculate the destination point, using redash. */ - const isDraggingDown = translationY > 0; - - const destinationPoint = (() => { - const endingSnapPoint = snapPoint( - translationY + context.initialPosition, - velocityY, - snapPoints - ); - if (source === GESTURE_SOURCE.HANDLE) { - return endingSnapPoint; - } - const secondHighestSnapPoint = - animatedSnapPoints.value[animatedSnapPoints.value.length - 2]; - return isDraggingDown - ? Math.min(secondHighestSnapPoint, endingSnapPoint) - : endingSnapPoint; - })(); + const destinationPoint = snapPoint( + translationY + context.value.initialPosition, + velocityY, + snapPoints + ); /** * if destination point is the same as the current position, @@ -316,7 +353,7 @@ export const useCustomGestureEventsHandlers = () => { } const wasGestureHandledByScrollView = - source === GESTURE_SOURCE.SCROLLABLE && + source === GESTURE_SOURCE.CONTENT && animatedScrollableContentOffsetY.value > 0; /** * prevents snapping from top to middle / bottom with repeated interrupted scrolls @@ -333,7 +370,8 @@ export const useCustomGestureEventsHandlers = () => { }, [ enablePanDownToClose, - animateToPosition, + isInTemporaryPosition, + isScrollableRefreshable, animatedClosedPosition, animatedHighestSnapPoint, animatedKeyboardHeight, @@ -341,14 +379,21 @@ export const useCustomGestureEventsHandlers = () => { animatedScrollableType, animatedSnapPoints, animatedScrollableContentOffsetY, - isInTemporaryPosition, - isScrollableRefreshable, + animateToPosition, ] ); + const handleOnFinalize: GestureEventHandlerCallbackType = useWorkletCallback( + function handleOnFinalize() { + resetContext(context); + }, + [context] + ); + //#endregion return { handleOnStart, - handleOnActive, + handleOnChange, handleOnEnd, + handleOnFinalize, }; }; diff --git a/src/hooks/useGestureHandler.ts b/src/hooks/useGestureHandler.ts index eb50e72be..c1fdfec8f 100644 --- a/src/hooks/useGestureHandler.ts +++ b/src/hooks/useGestureHandler.ts @@ -1,96 +1,86 @@ -import Animated, { useAnimatedGestureHandler } from 'react-native-reanimated'; import { + type GestureStateChangeEvent, + type GestureUpdateEvent, + type PanGestureChangeEventPayload, + type PanGestureHandlerEventPayload, State, - PanGestureHandlerGestureEvent, } from 'react-native-gesture-handler'; +import type { SharedValue } from 'react-native-reanimated'; +import { useWorkletCallback } from 'react-native-reanimated'; import { GESTURE_SOURCE } from '../constants'; import type { - GestureEventContextType, GestureEventHandlerCallbackType, + GestureHandlersHookType, } from '../types'; -const resetContext = (context: any) => { - 'worklet'; +export const useGestureHandler: GestureHandlersHookType = ( + source: GESTURE_SOURCE, + state: SharedValue, + gestureSource: SharedValue, + onStart: GestureEventHandlerCallbackType, + onChange: GestureEventHandlerCallbackType, + onEnd: GestureEventHandlerCallbackType, + onFinalize: GestureEventHandlerCallbackType +) => { + const handleOnStart = useWorkletCallback( + (event: GestureStateChangeEvent) => { + state.value = State.BEGAN; + gestureSource.value = source; - Object.keys(context).map(key => { - context[key] = undefined; - }); -}; - -export const useGestureHandler = ( - type: GESTURE_SOURCE, - state: Animated.SharedValue, - gestureSource: Animated.SharedValue, - handleOnStart: GestureEventHandlerCallbackType, - handleOnActive: GestureEventHandlerCallbackType, - handleOnEnd: GestureEventHandlerCallbackType -): ((event: PanGestureHandlerGestureEvent) => void) => { - const gestureHandler = useAnimatedGestureHandler< - PanGestureHandlerGestureEvent, - GestureEventContextType - >( - { - onActive: (payload, context) => { - if (!context.didStart) { - context.didStart = true; - - state.value = State.BEGAN; - gestureSource.value = type; - - handleOnStart(type, payload, context); - return; - } - - if (gestureSource.value !== type) { - return; - } - - state.value = payload.state; - handleOnActive(type, payload, context); - }, - onEnd: (payload, context) => { - if (gestureSource.value !== type) { - return; - } + onStart(source, event); + return; + }, + [state, gestureSource, source, onStart] + ); - state.value = payload.state; - gestureSource.value = GESTURE_SOURCE.UNDETERMINED; + const handleOnChange = useWorkletCallback( + ( + event: GestureUpdateEvent< + PanGestureHandlerEventPayload & PanGestureChangeEventPayload + > + ) => { + if (gestureSource.value !== source) { + return; + } - handleOnEnd(type, payload, context); - resetContext(context); - }, - onCancel: (payload, context) => { - if (gestureSource.value !== type) { - return; - } + state.value = event.state; + onChange(source, event); + }, + [state, gestureSource, source, onChange] + ); - state.value = payload.state; - gestureSource.value = GESTURE_SOURCE.UNDETERMINED; + const handleOnEnd = useWorkletCallback( + (event: GestureStateChangeEvent) => { + if (gestureSource.value !== source) { + return; + } - resetContext(context); - }, - onFail: (payload, context) => { - if (gestureSource.value !== type) { - return; - } + state.value = event.state; + gestureSource.value = GESTURE_SOURCE.UNDETERMINED; - state.value = payload.state; - gestureSource.value = GESTURE_SOURCE.UNDETERMINED; + onEnd(source, event); + }, + [state, gestureSource, source, onEnd] + ); - resetContext(context); - }, - onFinish: (payload, context) => { - if (gestureSource.value !== type) { - return; - } + const handleOnFinalize = useWorkletCallback( + (event: GestureStateChangeEvent) => { + if (gestureSource.value !== source) { + return; + } - state.value = payload.state; - gestureSource.value = GESTURE_SOURCE.UNDETERMINED; + state.value = event.state; + gestureSource.value = GESTURE_SOURCE.UNDETERMINED; - resetContext(context); - }, + onFinalize(source, event); }, - [type, state, handleOnStart, handleOnActive, handleOnEnd] + [state, gestureSource, source, onFinalize] ); - return gestureHandler; + + return { + handleOnStart, + handleOnChange, + handleOnEnd, + handleOnFinalize, + }; }; diff --git a/src/hooks/useKeyboard.ts b/src/hooks/useKeyboard.ts index 18293801b..cc7bdba39 100644 --- a/src/hooks/useKeyboard.ts +++ b/src/hooks/useKeyboard.ts @@ -1,9 +1,9 @@ import { useEffect } from 'react'; import { Keyboard, - KeyboardEvent, - KeyboardEventEasing, - KeyboardEventName, + type KeyboardEvent, + type KeyboardEventEasing, + type KeyboardEventName, Platform, } from 'react-native'; import { @@ -33,16 +33,22 @@ export const useKeyboard = () => { const keyboardState = useSharedValue( KEYBOARD_STATE.UNDETERMINED ); - const keyboardHeight = useSharedValue(0); + const keyboardHeight = useSharedValue(0); const keyboardAnimationEasing = useSharedValue('keyboard'); const keyboardAnimationDuration = useSharedValue(500); - const temporaryCachedKeyboardEvent = useSharedValue([]); + // biome-ignore lint: to be addressed! + const temporaryCachedKeyboardEvent = useSharedValue([]); //#endregion //#region worklets const handleKeyboardEvent = useWorkletCallback( - (state, height, duration, easing) => { + ( + state: KEYBOARD_STATE, + height: number, + duration: number, + easing: KeyboardEventEasing + ) => { if (state === KEYBOARD_STATE.SHOWN && !shouldHandleKeyboardEvents.value) { /** * if the keyboard event was fired before the `onFocus` on TextInput, @@ -53,11 +59,7 @@ export const useKeyboard = () => { return; } keyboardHeight.value = - state === KEYBOARD_STATE.SHOWN - ? height - : height === 0 - ? keyboardHeight.value - : height; + state === KEYBOARD_STATE.SHOWN ? height : keyboardHeight.value; keyboardAnimationDuration.value = duration; keyboardAnimationEasing.value = easing; keyboardState.value = state; @@ -114,7 +116,8 @@ export const useKeyboard = () => { if (result && params.length > 0) { handleKeyboardEvent(params[0], params[1], params[2], params[3]); } - } + }, + [] ); //#endregion diff --git a/src/hooks/useNormalizedSnapPoints.ts b/src/hooks/useNormalizedSnapPoints.ts deleted file mode 100644 index 31d8ff226..000000000 --- a/src/hooks/useNormalizedSnapPoints.ts +++ /dev/null @@ -1,46 +0,0 @@ -import Animated, { useDerivedValue } from 'react-native-reanimated'; -import { normalizeSnapPoint } from '../utilities'; -import type { BottomSheetProps } from '../components/bottomSheet'; -import { - INITIAL_CONTAINER_HEIGHT, - INITIAL_SNAP_POINT, -} from '../components/bottomSheet/constants'; - -/** - * Convert percentage snap points to pixels in screen and calculate - * the accurate snap points positions. - * @param providedSnapPoints provided snap points. - * @param containerHeight BottomSheetContainer height. - * @param topInset top inset. - * @param bottomInset bottom inset. - * @param $modal is sheet in a modal. - * @returns {Animated.SharedValue} - */ -export const useNormalizedSnapPoints = ( - providedSnapPoints: BottomSheetProps['snapPoints'], - containerHeight: Animated.SharedValue, - topInset: number, - bottomInset: number, - $modal: boolean -) => { - const normalizedSnapPoints = useDerivedValue(() => - ('value' in providedSnapPoints - ? providedSnapPoints.value - : providedSnapPoints - ).map(snapPoint => { - if (containerHeight.value === INITIAL_CONTAINER_HEIGHT) { - return INITIAL_SNAP_POINT; - } - - return normalizeSnapPoint( - snapPoint, - containerHeight.value, - topInset, - bottomInset, - $modal - ); - }) - ); - - return normalizedSnapPoints; -}; diff --git a/src/hooks/usePropsValidator.ts b/src/hooks/usePropsValidator.ts index ce6c400fa..9f23bfee7 100644 --- a/src/hooks/usePropsValidator.ts +++ b/src/hooks/usePropsValidator.ts @@ -1,7 +1,7 @@ -import { useMemo } from 'react'; import invariant from 'invariant'; -import { INITIAL_SNAP_POINT } from '../components/bottomSheet/constants'; +import { useMemo } from 'react'; import type { BottomSheetProps } from '../components/bottomSheet'; +import { INITIAL_SNAP_POINT } from '../components/bottomSheet/constants'; /** * @todo @@ -11,14 +11,22 @@ import type { BottomSheetProps } from '../components/bottomSheet'; export const usePropsValidator = ({ index, snapPoints, + enableDynamicSizing, topInset, bottomInset, -}: BottomSheetProps) => { +}: Pick< + BottomSheetProps, + 'index' | 'snapPoints' | 'enableDynamicSizing' | 'topInset' | 'bottomInset' +>) => { useMemo(() => { //#region snap points - const _snapPoints = 'value' in snapPoints ? snapPoints.value : snapPoints; + const _snapPoints = snapPoints + ? 'value' in snapPoints + ? snapPoints.value + : snapPoints + : []; invariant( - _snapPoints, + _snapPoints || enableDynamicSizing, `'snapPoints' was not provided! please provide at least one snap point.` ); @@ -26,7 +34,7 @@ export const usePropsValidator = ({ const _snapPoint = typeof snapPoint === 'number' ? snapPoint - : parseInt(snapPoint.replace('%', ''), 10); + : Number.parseInt(snapPoint.replace('%', ''), 10); invariant( _snapPoint > 0 || _snapPoint === INITIAL_SNAP_POINT, @@ -35,7 +43,7 @@ export const usePropsValidator = ({ }); invariant( - 'value' in _snapPoints || _snapPoints.length > 0, + 'value' in _snapPoints || _snapPoints.length > 0 || enableDynamicSizing, `'snapPoints' was provided with no points! please provide at least one snap point.` ); //#endregion @@ -47,9 +55,10 @@ export const usePropsValidator = ({ ); invariant( - typeof index === 'number' - ? index >= -1 && index <= _snapPoints.length - 1 - : true, + enableDynamicSizing || + (typeof index === 'number' + ? index >= -1 && index <= _snapPoints.length - 1 + : true), `'index' was provided but out of the provided snap points range! expected value to be between -1, ${ _snapPoints.length - 1 }` @@ -68,5 +77,5 @@ export const usePropsValidator = ({ //#endregion // animations - }, [index, snapPoints, topInset, bottomInset]); + }, [index, snapPoints, topInset, bottomInset, enableDynamicSizing]); }; diff --git a/src/hooks/useReactiveSharedValue.ts b/src/hooks/useReactiveSharedValue.ts index 05866c459..9dda7bde8 100644 --- a/src/hooks/useReactiveSharedValue.ts +++ b/src/hooks/useReactiveSharedValue.ts @@ -1,15 +1,13 @@ import { useEffect, useRef } from 'react'; -import Animated, { - cancelAnimation, - makeMutable, -} from 'react-native-reanimated'; +import type { SharedValue } from 'react-native-reanimated'; +import { cancelAnimation, makeMutable } from 'react-native-reanimated'; import type { Primitive } from '../types'; export const useReactiveSharedValue = ( value: T -): T extends Primitive ? Animated.SharedValue : T => { +): T extends Primitive ? SharedValue : T => { const initialValueRef = useRef(null); - const valueRef = useRef>(null); + const valueRef = useRef>(null); if (value && typeof value === 'object' && 'value' in value) { /** diff --git a/src/hooks/useScrollEventsHandlersDefault.ts b/src/hooks/useScrollEventsHandlersDefault.ts index dc83554ea..9c84807b8 100644 --- a/src/hooks/useScrollEventsHandlersDefault.ts +++ b/src/hooks/useScrollEventsHandlersDefault.ts @@ -1,10 +1,12 @@ -import { scrollTo, useWorkletCallback } from 'react-native-reanimated'; -import { useBottomSheetInternal } from './useBottomSheetInternal'; +import { State } from 'react-native-gesture-handler'; +import { scrollTo, useAnimatedReaction, useSharedValue, useWorkletCallback } from 'react-native-reanimated'; import { ANIMATION_STATE, SCROLLABLE_STATE, SHEET_STATE } from '../constants'; import type { - ScrollEventsHandlersHookType, ScrollEventHandlerCallbackType, + ScrollEventsHandlersHookType, } from '../types'; +import { useBottomSheetInternal } from './useBottomSheetInternal'; +import { Platform } from 'react-native'; export type ScrollEventContextType = { initialContentOffsetY: number; @@ -13,20 +15,55 @@ export type ScrollEventContextType = { export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( scrollableRef, - scrollableContentOffsetY + scrollableContentOffsetY, + scrollBuffer, + preserveScrollMomentum, + lockableScrollableContentOffsetY, ) => { // hooks const { animatedSheetState, animatedScrollableState, animatedAnimationState, + animatedHandleGestureState, animatedScrollableContentOffsetY: rootScrollableContentOffsetY, + isScrollableLocked, + isScrollEnded, } = useBottomSheetInternal(); + const awaitingFirstScroll = useSharedValue(false); + const _lockableScrollableContentOffsetY = useSharedValue(0); + + useAnimatedReaction( + () => _lockableScrollableContentOffsetY.value, + _lockableScrollableContentOffsetY => { + if (lockableScrollableContentOffsetY) { + lockableScrollableContentOffsetY.value = _lockableScrollableContentOffsetY; + } + } + ); //#region callbacks const handleOnScroll: ScrollEventHandlerCallbackType = useWorkletCallback( - (_, context) => { + ({ contentOffset: { y } }, context) => { + /** + * When a scrollBuffer is provided, we take locking the scrollable into our own hands. + * We need to do this because it's not possible to know the direction of the scroll during + * handleOnBeginDrag, and the scrollable shouldn't be locked when scrolling back to the + * start of the list. + */ + if ((preserveScrollMomentum || scrollBuffer) && awaitingFirstScroll.value && !isScrollableLocked.value) { + const isScrollingTowardsBottom = context.initialContentOffsetY < y; + if (isScrollingTowardsBottom && y > (scrollBuffer ?? 0) && context.shouldLockInitialPosition) { + isScrollableLocked.value = true; + animatedScrollableState.value = SCROLLABLE_STATE.LOCKED; + } else if (!isScrollingTowardsBottom && preserveScrollMomentum && y <= 0 && context.shouldLockInitialPosition) { + isScrollableLocked.value = true; + animatedScrollableState.value = SCROLLABLE_STATE.LOCKED; + } + awaitingFirstScroll.value = false; + } + /** * if sheet position is extended or fill parent, then we reset * `shouldLockInitialPosition` value to false. @@ -38,15 +75,28 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( context.shouldLockInitialPosition = false; } + /** + * if handle gesture state is active, then we capture the offset y position + * and lock the scrollable with it. + */ + if (animatedHandleGestureState.value === State.ACTIVE) { + context.shouldLockInitialPosition = true; + context.initialContentOffsetY = y; + } + if (animatedScrollableState.value === SCROLLABLE_STATE.LOCKED) { - const lockPosition = context.shouldLockInitialPosition - ? context.initialContentOffsetY ?? 0 - : 0; - // @ts-ignore - scrollTo(scrollableRef, 0, lockPosition, false); - scrollableContentOffsetY.value = lockPosition; + if (!(preserveScrollMomentum && isScrollEnded.value)) { + const lockPosition = context.shouldLockInitialPosition + ? context.initialContentOffsetY ?? 0 + : 0; + // @ts-ignore + scrollTo(scrollableRef, 0, lockPosition, false); + scrollableContentOffsetY.value = lockPosition; + _lockableScrollableContentOffsetY.value = lockPosition; + } return; } + _lockableScrollableContentOffsetY.value = y; }, [ scrollableRef, @@ -57,19 +107,46 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( ); const handleOnBeginDrag: ScrollEventHandlerCallbackType = useWorkletCallback( - ({ contentOffset: { y } }, context) => { + (event, context) => { + const y = event.contentOffset.y; scrollableContentOffsetY.value = y; + _lockableScrollableContentOffsetY.value = y; rootScrollableContentOffsetY.value = y; context.initialContentOffsetY = y; + awaitingFirstScroll.value = true; + isScrollEnded.value = false; + + if (scrollBuffer) { + if (y <= 0 && ( + animatedSheetState.value === SHEET_STATE.EXTENDED || + animatedSheetState.value === SHEET_STATE.FILL_PARENT + )) { + isScrollableLocked.value = true; + } else { + isScrollableLocked.value = false; + } + } else if (preserveScrollMomentum) { + if (Platform.OS === 'ios') { + isScrollableLocked.value = false; + } else { + // On Android, there is no overscroll, so the handleOnScroll + // callback never fires when dragging down from the top. In those cases, + // we just want to lock the scrollable immediately so that it can + // pan downward as expected. + isScrollableLocked.value = y <= 0; + } + } else { + isScrollableLocked.value = true; + } /** * if sheet position not extended or fill parent and the scrollable position * not at the top, then we should lock the initial scrollable position. */ if ( - animatedSheetState.value !== SHEET_STATE.EXTENDED && + (animatedSheetState.value !== SHEET_STATE.EXTENDED && animatedSheetState.value !== SHEET_STATE.FILL_PARENT && - y > 0 + y > 0) || (preserveScrollMomentum && y <= 0) ) { context.shouldLockInitialPosition = true; } else { @@ -84,18 +161,23 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( ); const handleOnEndDrag: ScrollEventHandlerCallbackType = useWorkletCallback( - ({ contentOffset: { y } }, context) => { + ({ contentOffset: { y }}, context) => { + awaitingFirstScroll.value = false; + isScrollEnded.value = true; if (animatedScrollableState.value === SCROLLABLE_STATE.LOCKED) { const lockPosition = context.shouldLockInitialPosition - ? context.initialContentOffsetY ?? 0 + ? (context.initialContentOffsetY ?? 0) : 0; // @ts-ignore scrollTo(scrollableRef, 0, lockPosition, false); scrollableContentOffsetY.value = lockPosition; + _lockableScrollableContentOffsetY.value = lockPosition; return; } + if (animatedAnimationState.value !== ANIMATION_STATE.RUNNING) { scrollableContentOffsetY.value = y; + _lockableScrollableContentOffsetY.value = y; rootScrollableContentOffsetY.value = y; } }, @@ -111,16 +193,21 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( useWorkletCallback( ({ contentOffset: { y } }, context) => { if (animatedScrollableState.value === SCROLLABLE_STATE.LOCKED) { - const lockPosition = context.shouldLockInitialPosition - ? context.initialContentOffsetY ?? 0 - : 0; - // @ts-ignore - scrollTo(scrollableRef, 0, lockPosition, false); - scrollableContentOffsetY.value = 0; + if (!(preserveScrollMomentum && isScrollEnded.value)) { + const lockPosition = context.shouldLockInitialPosition + ? context.initialContentOffsetY ?? 0 + : 0; + // @ts-ignore + scrollTo(scrollableRef, 0, lockPosition, false); + scrollableContentOffsetY.value = 0; + _lockableScrollableContentOffsetY.value = 0; + } return; } + if (animatedAnimationState.value !== ANIMATION_STATE.RUNNING) { scrollableContentOffsetY.value = y; + _lockableScrollableContentOffsetY.value = y; rootScrollableContentOffsetY.value = y; } }, diff --git a/src/hooks/useScrollHandler.ts b/src/hooks/useScrollHandler.ts index b82d3b976..1af5d66c6 100644 --- a/src/hooks/useScrollHandler.ts +++ b/src/hooks/useScrollHandler.ts @@ -1,18 +1,22 @@ import { + SharedValue, runOnJS, useAnimatedRef, useAnimatedScrollHandler, useSharedValue, } from 'react-native-reanimated'; -import { useScrollEventsHandlersDefault } from './useScrollEventsHandlersDefault'; -import { workletNoop as noop } from '../utilities'; import type { Scrollable, ScrollableEvent } from '../types'; +import { workletNoop as noop } from '../utilities'; +import { useScrollEventsHandlersDefault } from './useScrollEventsHandlersDefault'; export const useScrollHandler = ( useScrollEventsHandlers = useScrollEventsHandlersDefault, onScroll?: ScrollableEvent, onScrollBeginDrag?: ScrollableEvent, - onScrollEndDrag?: ScrollableEvent + onScrollEndDrag?: ScrollableEvent, + scrollBuffer?: number, + preserveScrollMomentum?: boolean, + lockableScrollableContentOffsetY?: SharedValue, ) => { // refs const scrollableRef = useAnimatedRef(); @@ -27,7 +31,7 @@ export const useScrollHandler = ( handleOnEndDrag = noop, handleOnMomentumEnd = noop, handleOnMomentumBegin = noop, - } = useScrollEventsHandlers(scrollableRef, scrollableContentOffsetY); + } = useScrollEventsHandlers(scrollableRef, scrollableContentOffsetY, scrollBuffer, preserveScrollMomentum, lockableScrollableContentOffsetY); // callbacks const scrollHandler = useAnimatedScrollHandler( diff --git a/src/hooks/useScrollHandler.web.ts b/src/hooks/useScrollHandler.web.ts new file mode 100644 index 000000000..4e6abeaeb --- /dev/null +++ b/src/hooks/useScrollHandler.web.ts @@ -0,0 +1,175 @@ +import { type TouchEvent, useEffect, useRef } from 'react'; +import { findNodeHandle } from 'react-native'; +import { useSharedValue } from 'react-native-reanimated'; +import { ANIMATION_STATE, SCROLLABLE_STATE } from '../constants'; +import type { Scrollable, ScrollableEvent } from '../types'; +import { useBottomSheetInternal } from './useBottomSheetInternal'; + +export type ScrollEventContextType = { + initialContentOffsetY: number; + shouldLockInitialPosition: boolean; +}; + +export const useScrollHandler = (_: never, onScroll?: ScrollableEvent) => { + //#region refs + const scrollableRef = useRef(null); + //#endregion + + //#region variables + const scrollableContentOffsetY = useSharedValue(0); + //#endregion + + //#region hooks + const { + animatedScrollableState, + animatedAnimationState, + animatedScrollableContentOffsetY, + } = useBottomSheetInternal(); + //#endregion + + //#region effects + useEffect(() => { + // biome-ignore lint: to be addressed! + const element = findNodeHandle(scrollableRef.current) as any; + + let scrollOffset = 0; + let supportsPassive = false; + let maybePrevent = false; + let lastTouchY = 0; + + let initialContentOffsetY = 0; + const shouldLockInitialPosition = false; + + function handleOnTouchStart(event: TouchEvent) { + if (event.touches.length !== 1) { + return; + } + + initialContentOffsetY = element.scrollTop; + lastTouchY = event.touches[0].clientY; + maybePrevent = scrollOffset <= 0; + } + + function handleOnTouchMove(event: TouchEvent) { + if (animatedScrollableState.value === SCROLLABLE_STATE.LOCKED) { + return event.preventDefault(); + } + + if (maybePrevent) { + maybePrevent = false; + + const touchY = event.touches[0].clientY; + const touchYDelta = touchY - lastTouchY; + + if (touchYDelta > 0) { + return event.preventDefault(); + } + } + + return true; + } + + function handleOnTouchEnd() { + if (animatedScrollableState.value === SCROLLABLE_STATE.LOCKED) { + const lockPosition = shouldLockInitialPosition + ? (initialContentOffsetY ?? 0) + : 0; + element.scroll({ + top: 0, + left: 0, + behavior: 'instant', + }); + scrollableContentOffsetY.value = lockPosition; + return; + } + } + + function handleOnScroll(event: TouchEvent) { + scrollOffset = element.scrollTop; + + if (animatedAnimationState.value !== ANIMATION_STATE.RUNNING) { + scrollableContentOffsetY.value = Math.max(0, scrollOffset); + animatedScrollableContentOffsetY.value = Math.max(0, scrollOffset); + } + + if (scrollOffset <= 0) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + return true; + } + + try { + // @ts-ignore + window.addEventListener('test', null, { + // @ts-ignore + // biome-ignore lint: to be addressed + get passive() { + supportsPassive = true; + }, + }); + } catch (_e) {} + + element.addEventListener( + 'touchstart', + handleOnTouchStart, + supportsPassive + ? { + passive: true, + } + : false + ); + + element.addEventListener( + 'touchmove', + handleOnTouchMove, + supportsPassive + ? { + passive: false, + } + : false + ); + + element.addEventListener( + 'touchend', + handleOnTouchEnd, + supportsPassive + ? { + passive: false, + } + : false + ); + + element.addEventListener( + 'scroll', + handleOnScroll, + supportsPassive + ? { + passive: false, + } + : false + ); + + return () => { + // @ts-ignore + window.removeEventListener('test', null); + element.removeEventListener('touchstart', handleOnTouchStart); + element.removeEventListener('touchmove', handleOnTouchMove); + element.removeEventListener('touchend', handleOnTouchEnd); + element.removeEventListener('scroll', handleOnScroll); + }; + }, [ + animatedAnimationState, + animatedScrollableContentOffsetY, + animatedScrollableState, + scrollableContentOffsetY, + ]); + //#endregion + + return { + scrollHandler: onScroll, + scrollableRef, + scrollableContentOffsetY, + }; +}; diff --git a/src/hooks/useScrollable.ts b/src/hooks/useScrollable.ts index 396317575..9824fde9b 100644 --- a/src/hooks/useScrollable.ts +++ b/src/hooks/useScrollable.ts @@ -1,8 +1,8 @@ -import { useCallback, RefObject, useRef } from 'react'; +import { type RefObject, useCallback, useRef } from 'react'; +import { type NodeHandle, findNodeHandle } from 'react-native'; import { useSharedValue } from 'react-native-reanimated'; -import { getRefNativeTag } from '../utilities/getRefNativeTag'; import { SCROLLABLE_STATE, SCROLLABLE_TYPE } from '../constants'; -import type { ScrollableRef, Scrollable } from '../types'; +import type { Scrollable, ScrollableRef } from '../types'; export const useScrollable = () => { // refs @@ -18,11 +18,13 @@ export const useScrollable = () => { SCROLLABLE_STATE.UNDETERMINED ); const isScrollableRefreshable = useSharedValue(false); + const isScrollableLocked = useSharedValue(true); + const isScrollEnded = useSharedValue(true); // callbacks const setScrollableRef = useCallback((ref: ScrollableRef) => { // get current node handle id - let currentRefId = scrollableRef.current?.id ?? null; + const currentRefId = scrollableRef.current?.id ?? null; if (currentRefId !== ref.id) { if (scrollableRef.current) { @@ -36,15 +38,15 @@ export const useScrollable = () => { const removeScrollableRef = useCallback((ref: RefObject) => { // find node handle id - let id; + let id: NodeHandle | null; try { - id = getRefNativeTag(ref); + id = findNodeHandle(ref.current); } catch { return; } // get current node handle id - let currentRefId = scrollableRef.current?.id ?? null; + const currentRefId = scrollableRef.current?.id ?? null; /** * @DEV @@ -63,6 +65,8 @@ export const useScrollable = () => { animatedScrollableContentOffsetY, animatedScrollableOverrideState, isScrollableRefreshable, + isScrollableLocked, + isScrollEnded, setScrollableRef, removeScrollableRef, }; diff --git a/src/hooks/useScrollableSetter.ts b/src/hooks/useScrollableSetter.ts index ea7d3c24e..90c8bbad2 100644 --- a/src/hooks/useScrollableSetter.ts +++ b/src/hooks/useScrollableSetter.ts @@ -1,15 +1,19 @@ -import React, { useCallback, useEffect } from 'react'; -import Animated from 'react-native-reanimated'; -import { useBottomSheetInternal } from './useBottomSheetInternal'; -import { getRefNativeTag } from '../utilities/getRefNativeTag'; -import { SCROLLABLE_TYPE } from '../constants'; +import type React from 'react'; +import { useCallback, useEffect } from 'react'; +import { findNodeHandle } from 'react-native'; +import type { SharedValue } from 'react-native-reanimated'; +import type { SCROLLABLE_TYPE } from '../constants'; import type { Scrollable } from '../types'; +import { useBottomSheetInternal } from './useBottomSheetInternal'; +import { Platform } from 'react-native'; export const useScrollableSetter = ( ref: React.RefObject, type: SCROLLABLE_TYPE, - contentOffsetY: Animated.SharedValue, + contentOffsetY: SharedValue, refreshable: boolean, + scrollBuffer: number | undefined, + preserveScrollMomentum: boolean | undefined, useFocusHook = useEffect ) => { // hooks @@ -18,8 +22,12 @@ export const useScrollableSetter = ( animatedScrollableContentOffsetY: rootScrollableContentOffsetY, isContentHeightFixed, isScrollableRefreshable, + isScrollableLocked, setScrollableRef, removeScrollableRef, + animatedContainerHeight, + animatedContentHeight, + isScrollEnded, } = useBottomSheetInternal(); // callbacks @@ -28,10 +36,13 @@ export const useScrollableSetter = ( rootScrollableContentOffsetY.value = contentOffsetY.value; animatedScrollableType.value = type; isScrollableRefreshable.value = refreshable; + // Keep isScrollableLocked value if the scrollable is still scrolling + // Android scrollview doesn't bounce so we need to set isScrollableLocked so that the sheet can be pulled up/down + isScrollableLocked.value = (!isScrollEnded.value && isScrollableLocked.value) || (!preserveScrollMomentum && !scrollBuffer) || (Platform.OS === 'android' && animatedContentHeight.value <= animatedContainerHeight.value); isContentHeightFixed.value = false; // set current scrollable ref - const id = getRefNativeTag(ref); + const id = findNodeHandle(ref.current); if (id) { setScrollableRef({ id: id, @@ -52,6 +63,7 @@ export const useScrollableSetter = ( rootScrollableContentOffsetY, contentOffsetY, isScrollableRefreshable, + isScrollableLocked, isContentHeightFixed, setScrollableRef, removeScrollableRef, diff --git a/src/hooks/useStableCallback.ts b/src/hooks/useStableCallback.ts index a868620d5..3a9546de8 100644 --- a/src/hooks/useStableCallback.ts +++ b/src/hooks/useStableCallback.ts @@ -1,19 +1,25 @@ -import { useRef, useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'; + +// biome-ignore lint: to be addressed! +type Callback = (...args: T[]) => any; -type Callback = (...args: any[]) => any; /** - * Provide a stable version of useCallback - * https://gist.github.com/JakeCoxon/c7ebf6e6496f8468226fd36b596e1985 + * Provide a stable version of useCallback. */ -export const useStableCallback = (callback: Callback) => { - const callbackRef = useRef(); - const memoCallback = useCallback( - (...args) => callbackRef.current && callbackRef.current(...args), - [] - ); - useEffect(() => { +export function useStableCallback(callback: Callback) { + const callbackRef = useRef | undefined>(undefined); + + useLayoutEffect(() => { callbackRef.current = callback; - return () => (callbackRef.current = undefined); }); - return memoCallback; -}; + + useEffect(() => { + return () => { + callbackRef.current = undefined; + }; + }, []); + + return useCallback>((...args) => { + return callbackRef.current?.(...args); + }, []); +} diff --git a/src/index.ts b/src/index.ts index 68bb3abf6..5842fd37e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,6 @@ export { useBottomSheetSpringConfigs } from './hooks/useBottomSheetSpringConfigs export { useBottomSheetTimingConfigs } from './hooks/useBottomSheetTimingConfigs'; export { useBottomSheetInternal } from './hooks/useBottomSheetInternal'; export { useBottomSheetModalInternal } from './hooks/useBottomSheetModalInternal'; -export { useBottomSheetDynamicSnapPoints } from './hooks/useBottomSheetDynamicSnapPoints'; export { useScrollEventsHandlersDefault } from './hooks/useScrollEventsHandlersDefault'; export { useGestureEventsHandlersDefault } from './hooks/useGestureEventsHandlersDefault'; export { useBottomSheetGestureHandlers } from './hooks/useBottomSheetGestureHandlers'; @@ -26,6 +25,7 @@ export { BottomSheetSectionList, BottomSheetFlatList, BottomSheetVirtualizedList, + BottomSheetFlashList, } from './components/bottomSheetScrollable'; export { default as BottomSheetHandle } from './components/bottomSheetHandle'; export { default as BottomSheetDraggableView } from './components/bottomSheetDraggableView'; @@ -72,5 +72,6 @@ export type { //#region utilities export * from './constants'; +export { getKeyboardAnimationConfigs } from './utilities'; export { enableLogging } from './utilities/logger'; //#endregion diff --git a/src/types.d.ts b/src/types.d.ts index 803ac5097..b76731952 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,20 +1,26 @@ import type React from 'react'; import type { + AccessibilityProps, FlatList, - ScrollView, - SectionList, NativeScrollEvent, NativeSyntheticEvent, + ScrollView, + SectionList, } from 'react-native'; import type { GestureEventPayload, + GestureStateChangeEvent, + GestureUpdateEvent, + PanGestureChangeEventPayload, PanGestureHandlerEventPayload, + State, } from 'react-native-gesture-handler'; import type { SharedValue, WithSpringConfig, WithTimingConfig, } from 'react-native-reanimated'; +import type { FlashList } from '@shopify/flash-list'; import type { GESTURE_SOURCE } from './constants'; //#region Methods @@ -76,12 +82,14 @@ export interface BottomSheetMethods { */ forceClose: (animationConfigs?: WithSpringConfig | WithTimingConfig) => void; } -export interface BottomSheetModalMethods extends BottomSheetMethods { + +// biome-ignore lint/suspicious/noExplicitAny: Using 'any' allows users to define their own strict types for 'data' property. +export interface BottomSheetModalMethods extends BottomSheetMethods { /** * Mount and present the bottom sheet modal to the initial snap point. * @param data to be passed to the modal. */ - present: (data?: any) => void; + present: (data?: T) => void; /** * Close and unmount the bottom sheet modal. * @param animationConfigs snap animation configs. @@ -107,7 +115,7 @@ export interface BottomSheetVariables { } //#region scrollables -export type Scrollable = FlatList | ScrollView | SectionList; +export type Scrollable = FlashList | FlatList | ScrollView | SectionList; export type ScrollableRef = { id: number; node: React.RefObject; @@ -119,12 +127,6 @@ export type ScrollableEvent = ( //#region utils export type Primitive = string | number | boolean; -export interface Insets { - top: number; - bottom: number; - left: number; - right: number; -} //#endregion //#region hooks @@ -135,26 +137,54 @@ export type GestureEventContextType = { didStart?: boolean; }; -export type GestureEventHandlerCallbackType = ( +export type GestureEventHandlerCallbackType = ( source: GESTURE_SOURCE, - payload: GestureEventPayloadType, - context: C + payload: GestureEventPayloadType ) => void; export type GestureEventsHandlersHookType = () => { handleOnStart: GestureEventHandlerCallbackType; - handleOnActive: GestureEventHandlerCallbackType; + handleOnChange: GestureEventHandlerCallbackType; handleOnEnd: GestureEventHandlerCallbackType; + handleOnFinalize: GestureEventHandlerCallbackType; }; -type ScrollEventHandlerCallbackType = ( +export type GestureHandlersHookType = ( + source: GESTURE_SOURCE, + state: SharedValue, + gestureSource: SharedValue, + onStart: GestureEventHandlerCallbackType, + onChange: GestureEventHandlerCallbackType, + onEnd: GestureEventHandlerCallbackType, + onFinalize: GestureEventHandlerCallbackType +) => { + handleOnStart: ( + event: GestureStateChangeEvent + ) => void; + handleOnChange: ( + event: GestureUpdateEvent< + PanGestureHandlerEventPayload & PanGestureChangeEventPayload + > + ) => void; + handleOnEnd: ( + event: GestureStateChangeEvent + ) => void; + handleOnFinalize: ( + event: GestureStateChangeEvent + ) => void; +}; + +type ScrollEventHandlerCallbackType = ( payload: NativeScrollEvent, context: C ) => void; export type ScrollEventsHandlersHookType = ( ref: React.RefObject, - contentOffsetY: SharedValue + contentOffsetY: SharedValue, + scrollBuffer: number | undefined, + preserveScrollMomentum: boolean | undefined, + lockableScrollableContentOffsetY: SharedValue | undefined ) => { handleOnScroll?: ScrollEventHandlerCallbackType; handleOnBeginDrag?: ScrollEventHandlerCallbackType; @@ -163,3 +193,10 @@ export type ScrollEventsHandlersHookType = ( handleOnMomentumEnd?: ScrollEventHandlerCallbackType; }; //#endregion + +export interface NullableAccessibilityProps extends AccessibilityProps { + accessible?: AccessibilityProps['accessible'] | null; + accessibilityLabel?: AccessibilityProps['accessibilityLabel'] | null; + accessibilityHint?: AccessibilityProps['accessibilityHint'] | null; + accessibilityRole?: AccessibilityProps['accessibilityRole'] | null; +} diff --git a/src/utilities/animate.ts b/src/utilities/animate.ts index 0ce4c9a50..b1f6fb431 100644 --- a/src/utilities/animate.ts +++ b/src/utilities/animate.ts @@ -1,9 +1,10 @@ import { - WithSpringConfig, - WithTimingConfig, - withTiming, + type AnimationCallback, + type ReduceMotion, + type WithSpringConfig, + type WithTimingConfig, withSpring, - AnimationCallback, + withTiming, } from 'react-native-reanimated'; import { ANIMATION_CONFIGS, ANIMATION_METHOD } from '../constants'; @@ -11,13 +12,15 @@ interface AnimateParams { point: number; velocity?: number; configs?: WithSpringConfig | WithTimingConfig; + overrideReduceMotion?: ReduceMotion; onComplete?: AnimationCallback; } export const animate = ({ point, - configs = undefined, + configs, velocity = 0, + overrideReduceMotion, onComplete, }: AnimateParams) => { 'worklet'; @@ -26,6 +29,15 @@ export const animate = ({ configs = ANIMATION_CONFIGS; } + // Users might have an accessibility setting to reduce motion turned on. + // This prevents the animation from running when presenting the sheet, which results in + // the bottom sheet not even appearing so we need to override it to ensure the animation runs. + // configs.reduceMotion = ReduceMotion.Never; + + if (overrideReduceMotion) { + configs.reduceMotion = overrideReduceMotion; + } + // detect animation type const type = 'duration' in configs || 'easing' in configs @@ -34,11 +46,11 @@ export const animate = ({ if (type === ANIMATION_METHOD.TIMING) { return withTiming(point, configs as WithTimingConfig, onComplete); - } else { - return withSpring( - point, - Object.assign({ velocity }, configs) as WithSpringConfig, - onComplete - ); } + + return withSpring( + point, + Object.assign({ velocity }, configs) as WithSpringConfig, + onComplete + ); }; diff --git a/src/utilities/getKeyboardAnimationConfigs.ts b/src/utilities/getKeyboardAnimationConfigs.ts index de5a2a48b..13103474c 100644 --- a/src/utilities/getKeyboardAnimationConfigs.ts +++ b/src/utilities/getKeyboardAnimationConfigs.ts @@ -1,5 +1,5 @@ -import { Easing } from 'react-native-reanimated'; import type { KeyboardEventEasing } from 'react-native'; +import { Easing } from 'react-native-reanimated'; export const getKeyboardAnimationConfigs = ( easing: KeyboardEventEasing, diff --git a/src/utilities/getRefNativeTag.ts b/src/utilities/getRefNativeTag.ts deleted file mode 100644 index d4f3cc42f..000000000 --- a/src/utilities/getRefNativeTag.ts +++ /dev/null @@ -1,43 +0,0 @@ -const isFunction = (ref: unknown): ref is Function => typeof ref === 'function'; - -const hasNativeTag = ( - ref: unknown -): ref is { current: { _nativeTag: number } } => - !!ref && - typeof ref === 'object' && - 'current' in (ref || {}) && - '_nativeTag' in ((ref as any)?.current || {}); - -/* - * getRefNativeTag is an internal utility used by createBottomSheetScrollableComponent - * to grab the native tag from the native host component. It only works when the ref - * is pointing to a native Host component. - * - * Internally in the bottom-sheet library ref can be a function that returns a native tag - * this seems to happen due to the usage of Reanimated's animated scroll components. - * - * This should be Fabric compatible as long as the ref is a native host component. - * */ -export function getRefNativeTag(ref: unknown) { - const refType = typeof ref; - let nativeTag: undefined | number; - if (isFunction(ref)) { - nativeTag = ref(); - } else if (hasNativeTag(ref)) { - nativeTag = ref.current._nativeTag; - } - - if (!nativeTag || typeof nativeTag !== 'number') { - throw new Error( - `Unexpected nativeTag: ${refType}; nativeTag=${nativeTag} - - createBottomSheetScrollableComponent's ScrollableComponent needs to return - a reference that contains a nativeTag to a Native HostComponent. - - ref=${ref} - ` - ); - } - - return nativeTag; -} diff --git a/src/utilities/getRefNativeTag.web.ts b/src/utilities/getRefNativeTag.web.ts new file mode 100644 index 000000000..82ae671dc --- /dev/null +++ b/src/utilities/getRefNativeTag.web.ts @@ -0,0 +1,6 @@ +import type { RefObject } from 'react'; +import { findNodeHandle } from 'react-native'; + +export function getRefNativeTag(ref: RefObject) { + return findNodeHandle(ref?.current) || null; +} diff --git a/src/utilities/logger.ts b/src/utilities/logger.ts index 46263cfce..7405d5533 100644 --- a/src/utilities/logger.ts +++ b/src/utilities/logger.ts @@ -1,28 +1,41 @@ interface PrintOptions { component?: string; + category?: 'layout' | 'effect' | 'callback'; method?: string; - params?: Record | string | number | boolean; + params?: Record | string | number | boolean; } type Print = (options: PrintOptions) => void; -let isLoggingEnabled = false; +let _isLoggingEnabled = false; +let _excludeCategories: PrintOptions['category'][] | undefined; -const enableLogging = () => { +const enableLogging = (excludeCategories?: PrintOptions['category'][]) => { if (!__DEV__) { console.warn('[BottomSheet] could not enable logging on production!'); return; } - isLoggingEnabled = true; + + _isLoggingEnabled = true; + _excludeCategories = excludeCategories; }; let print: Print = () => {}; if (__DEV__) { - print = ({ component, method, params }) => { - if (!isLoggingEnabled) { + print = ({ component, method, params, category }) => { + if (!_isLoggingEnabled) { return; } + + if ( + category && + _excludeCategories && + _excludeCategories.includes(category) + ) { + return; + } + let message = ''; if (typeof params === 'object') { @@ -32,7 +45,7 @@ if (__DEV__) { } else { message = `${params ?? ''}`; } - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: used for debugging console.log(`[${[component, method].filter(Boolean).join('::')}]`, message); }; } diff --git a/src/utilities/normalizeSnapPoint.ts b/src/utilities/normalizeSnapPoint.ts index 406fcd1cc..46745dbf9 100644 --- a/src/utilities/normalizeSnapPoint.ts +++ b/src/utilities/normalizeSnapPoint.ts @@ -3,10 +3,7 @@ */ export const normalizeSnapPoint = ( snapPoint: number | string, - containerHeight: number, - _topInset: number, - _bottomInset: number, - _$modal: boolean = false + containerHeight: number ) => { 'worklet'; let normalizedSnapPoint = snapPoint; diff --git a/src/utilities/validateSnapPoint.ts b/src/utilities/validateSnapPoint.ts index 1a854a82d..8b6d7534b 100644 --- a/src/utilities/validateSnapPoint.ts +++ b/src/utilities/validateSnapPoint.ts @@ -1,6 +1,6 @@ import invariant from 'invariant'; -export const validateSnapPoint = (snapPoint: any) => { +export const validateSnapPoint = (snapPoint: number | string) => { invariant( typeof snapPoint === 'number' || typeof snapPoint === 'string', `'${snapPoint}' is not a valid snap point! expected types are string or number.` diff --git a/templates/changelog-template.hbs b/templates/changelog-template.hbs deleted file mode 100644 index 7ae6cd09b..000000000 --- a/templates/changelog-template.hbs +++ /dev/null @@ -1,122 +0,0 @@ -## Changelog - -{{!-- -Introduction -• This template tries to follow conventional commits format https://www.conventionalcommits.org/en/v1.0.0/ -• The template uses regex to filter commit types into their own headings (this is more than just fixes and features headings) -• It also uses the replaceText function in package.json to remove the commit type text from the message, because the headers are shown instead. - -• The text 'Breaking:' or 'Breaking changes:' can be located anywhere in the commit. -• The types feat:, fix:, chore:, docs:, refactor:, test:, style:, perf: must be at the beginning of the commit subject with an : on end. - • They can optionally have a scope set to outline the module or component that is affected eg feat(bldAssess): -• There is a short hash on the end of every commit that is currently commented out so that change log did not grow too long (due to some system's file size limitations). You can uncomment if you wish [`{{shorthash}}`]({{href}}) - -Example Definitions -• feat: A new feature -• fix: A bug fix -• perf: A code change that improves performance -• refactor: A code change that neither fixes a bug nor adds a feature -• style: Changes that do not affect the meaning of the code (white-space, formatting, spelling mistakes, missing semi-colons, etc) -• test: Adding missing tests or correcting existing tests -• docs: Adding/updating documentation -• chore: Something like updating a library version, or moving files to be in a better location and updating all file refs ---}} - - -{{!-- In package.json need to add this to remove label text from the change log output (because the markdown headers are now used to group them). - NOTES • Individual brackets have been escaped twice to be Json compliant. - • For items that define a scope eg feat(bldAssess): We remove the 1st bracket and then re-add it so we can select the right piece of text -{ - "name": "my-awesome-package", - - "auto-changelog": { - "replaceText": { - "([bB]reaking:)": "", - "([bB]reaking change:)": "", - "(^[fF]eat:)": "", - "(^[fF]eat\\()": "\\(", - "(^[fF]ix:)": "", - "(^[fF]ix\\()": "\\(", - "(^[cC]hore:)": "", - "(^[cC]hore\\()": "\\(", - "(^[dD]ocs:)": "", - "(^[dD]ocs\\()": "\\(", - "(^[rR]efactor:)": "", - "(^[rR]efactor\\()": "\\(", - "(^[tT]est:)": "", - "(^[tT]est\\()": "\\(", - "(^[sS]tyle:)": "", - "(^[sS]tyle\\()": "\\(", - "(^[pP]erf:)": "", - "(^[pP]erf\\()": "\\(" - } - } - -} - --}} - - {{!-- - Regex reminders - ^ = starts with - \( = ( character (otherwise it is interpreted as a regex lookup group) - * = zero or more of the previous character - \s = whitespace - . = any character except newline - | = or - [aA] = character a or character A - --}} - - -{{#each releases}} - {{#if href}} - ##{{#unless major}}#{{/unless}} [{{title}}]({{href}}) - {{#if tag}} {{niceDate}} {{/if}} - - {{else}} - ### {{title}} - {{/if}} - - {{#if summary}} - {{summary}} - {{/if}} - - {{#custom merges fixes commits heading='#### Breaking Changes :warning:' message='[bB]reaking [cC]hange:|[bB]reaking:' }} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### New Features' message='^[fF]eat:|[fF]eat\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Fixes' message='^[fF]ix:|^[fF]ix\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Documentation Changes' message='^[dD]ocs:|^[dD]ocs\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Refactoring and Updates' message='^[rR]efactor:|^[rR]efactor\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Changes to Test Assets' message='^[tT]est:|^[tT]est\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Tidying of Code eg Whitespace' message='^[sS]tyle:|^[sS]tyle\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Performance Improvements' message='^[pP]erf:|^[pP]erf\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Chores And Housekeeping' message='^[cC]hore:|^[cC]hore\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### General Changes' exclude='[bB]reaking [cC]hange:|[bB]reaking:|^[fF]eat:|^[fF]eat\(|^[fF]ix:|^[fF]ix\(|^[cC]hore:|^[cC]hore\(|^[dD]ocs:|^[dD]ocs\(|^[rR]efactor:|^[rR]efactor\(|^[tT]est:|^[tT]est\(|^[sS]tyle:|^[sS]tyle\(|^[pP]erf:|^[pP]erf\('}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - -{{/each}} \ No newline at end of file diff --git a/templates/release-template.hbs b/templates/release-template.hbs deleted file mode 100644 index 9e53b3e8b..000000000 --- a/templates/release-template.hbs +++ /dev/null @@ -1,43 +0,0 @@ -{{#each releases}} - {{#if @first}} - {{#custom merges fixes commits heading='#### Breaking Changes :warning:' message='[bB]reaking [cC]hange:|[bB]reaking:' }} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### New Features' message='^[fF]eat:|[fF]eat\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Fixes' message='^[fF]ix:|^[fF]ix\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Documentation Changes' message='^[dD]ocs:|^[dD]ocs\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Refactoring and Updates' message='^[rR]efactor:|^[rR]efactor\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Changes to Test Assets' message='^[tT]est:|^[tT]est\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Tidying of Code eg Whitespace' message='^[sS]tyle:|^[sS]tyle\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Performance Improvements' message='^[pP]erf:|^[pP]erf\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### Chores And Housekeeping' message='^[cC]hore:|^[cC]hore\(' exclude='[bB]reaking [cC]hange:|[bB]reaking:'}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - - {{#custom merges fixes commits heading='#### General Changes' exclude='[bB]reaking [cC]hange:|[bB]reaking:|^[fF]eat:|^[fF]eat\(|^[fF]ix:|^[fF]ix\(|^[cC]hore:|^[cC]hore\(|^[dD]ocs:|^[dD]ocs\(|^[rR]efactor:|^[rR]efactor\(|^[tT]est:|^[tT]est\(|^[sS]tyle:|^[sS]tyle\(|^[pP]erf:|^[pP]erf\('}} - - {{subject}} ([`{{shorthash}}`]({{href}})) - {{/custom}} - {{/if}} -{{/each}} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 310b40ba9..d6860d495 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,9 @@ "resolveJsonModule": true, "skipLibCheck": true, "strict": true, - "target": "esnext" + "target": "esnext", + "importsNotUsedAsValues": "error", + "ignoreDeprecations": "5.0" }, "exclude": ["example"], "include": ["src"] diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 000000000..b2d6de306 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/website/README.md b/website/README.md new file mode 100644 index 000000000..0c6c2c27b --- /dev/null +++ b/website/README.md @@ -0,0 +1,41 @@ +# Website + +This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. + +### Installation + +``` +$ yarn +``` + +### Local Development + +``` +$ yarn start +``` + +This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. + +### Build + +``` +$ yarn build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + +### Deployment + +Using SSH: + +``` +$ USE_SSH=true yarn deploy +``` + +Not using SSH: + +``` +$ GIT_USER= yarn deploy +``` + +If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/website/babel.config.js b/website/babel.config.js new file mode 100644 index 000000000..e00595dae --- /dev/null +++ b/website/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], +}; diff --git a/website/blog/2021-08-30-bottom-sheet-v4.mdx b/website/blog/2021-08-30-bottom-sheet-v4.mdx new file mode 100644 index 000000000..bbdf81653 --- /dev/null +++ b/website/blog/2021-08-30-bottom-sheet-v4.mdx @@ -0,0 +1,136 @@ +--- +title: BottomSheet v4 is here! +description: BottomSheet v4 comes with rewritten implementation to provide more stability, performance, and more features. +slug: bottom-sheet-v4 +authors: + - gorhom +keywords: + - bottomsheet + - bottom-sheet + - bottom sheet + - react-native + - react native + - ios + - android + - sheet + - modal + - presentation modal + - reanimated +tags: [release] +image: /img/bottom-sheet-preview.gif +hide_table_of_contents: false +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Video from "@theme/Video"; + +Today I am releasing the `BottomSheet v4`, with a rewritten implementation to provide more stability, performance, and more features. + +{/* truncate */} + +## Features + +In this release, I have rewritten the implementation to 100% utilize `Reanimated v2` hooks and variables instead of using the JS once. This allows for more customization and provides more stability overall. + +### Keyboard Handling + +