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/.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..c7727d9b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,531 +1,166 @@ -## Changelog -### [v4.4.5](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.4.4...v4.4.5) - -#### Refactoring and Updates +## [5.1.6](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.1.5...v5.1.6) (2025-06-03) -- 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 +### Bug 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)) +* **#2267:** early exit when attempting to snap to index while layout is not ready ([0715f03](https://github.com/gorhom/react-native-bottom-sheet/commit/0715f0384a187cdb1df903d693666ac4b12db807)), closes [#2267](https://github.com/gorhom/react-native-bottom-sheet/issues/2267) +* **#2278:** removed flashlist for web ([e17096f](https://github.com/gorhom/react-native-bottom-sheet/commit/e17096feade145f9e6349815398f8aaae758d554)), closes [#2278](https://github.com/gorhom/react-native-bottom-sheet/issues/2278) +* added positions to onAnimate, and prevent index to be negative with keyboard animations ([#2271](https://github.com/gorhom/react-native-bottom-sheet/issues/2271))(by [@souyahia](https://github.com/souyahia)) ([898270e](https://github.com/gorhom/react-native-bottom-sheet/commit/898270e62e0f83c8f8df671a60d6aabe749d890e)) +* allow bottom sheet view to resize it self when its content resized ([5397478](https://github.com/gorhom/react-native-bottom-sheet/commit/53974786a18aceab1cc15def1b29c94ef93002e3)) +* updated BottomSheetModal mock, add createBottomSheetScrollableComponent and enum mocks ([#2265](https://github.com/gorhom/react-native-bottom-sheet/issues/2265))(by [@gabimoncha](https://github.com/gabimoncha)) ([a77904a](https://github.com/gorhom/react-native-bottom-sheet/commit/a77904ac935278bec4e086700e1e93baa54282b6)) -#### Refactoring and Updates +## [5.1.5](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.1.4...v5.1.5) (2025-05-26) -- 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 +### Bug Fixes -- added data to present modal api (#942) ([`8a3d138`](https://github.com/gorhom/react-native-bottom-sheet/commit/8a3d13871a40e08e0c3deb302b60bbb2bcffd9f3)) +* **#2237:** fixed node handle lookup for virtualized list on web (by [@btoo](https://github.com/btoo)) ([6442b0e](https://github.com/gorhom/react-native-bottom-sheet/commit/6442b0ea54a38d8dcb82f63aade077ead29d382b)) +* **#2288:** added unique id to the root bottom sheet modal portal ([711ea7a](https://github.com/gorhom/react-native-bottom-sheet/commit/711ea7a5290ef485b9ba5c65eb45e28d6e495b43)), closes [#2288](https://github.com/gorhom/react-native-bottom-sheet/issues/2288) +* fixed initial content height calculation on web ([4db946e](https://github.com/gorhom/react-native-bottom-sheet/commit/4db946e4af331bb2d3a80002ee6051da9f3593eb)) +* prevent canceling touchmove events when not cancelable ([#2244](https://github.com/gorhom/react-native-bottom-sheet/issues/2244))(by [@erickreutz](https://github.com/erickreutz)) ([14d5d1e](https://github.com/gorhom/react-native-bottom-sheet/commit/14d5d1e89f22b5101445799fd0cb836ecb7c4882)) +* provide the portal host name with use portal ([67e9097](https://github.com/gorhom/react-native-bottom-sheet/commit/67e909711164aba900c2764034723c8b0e051704)) -#### Refactoring and Updates +## [5.1.4](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.1.3...v5.1.4) (2025-05-04) -- 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 +### Bug Fixes -#### Fixes +* **#2237:** fixed recursive loop in findNodeHandle.web (by @TNAJanssen) ([3556ba8](https://github.com/gorhom/react-native-bottom-sheet/commit/3556ba8e1445a78dfc6cfc93997500d52a03368e)) -- allowed keyboard height to be recalculated when it changes (#931) ([`2f33bbe`](https://github.com/gorhom/react-native-bottom-sheet/commit/2f33bbe8ddee66b959100fbe06c54eaf097138df)) +## [5.1.3](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.1.2...v5.1.3) (2025-05-04) -### [v4.2.1](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.2.0...v4.2.1) - 24 April 2022 -#### Fixes +### Bug Fixes -- updated footer container export name ([`a887141`](https://github.com/gorhom/react-native-bottom-sheet/commit/a88714153a780395337b84efe00e3d410702c1d9)) +* **#2237:** updated findNodeHandle for web to support React 19 ([47a95f5](https://github.com/gorhom/react-native-bottom-sheet/commit/47a95f517ab5b4680d0f5a45b09464911aafd35e)), closes [#2237](https://github.com/gorhom/react-native-bottom-sheet/issues/2237) -### [v4.2.0](https://github.com/gorhom/react-native-bottom-sheet/compare/v4.1.6...v4.2.0) - 24 April 2022 +## [5.1.2](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.1.1...v5.1.2) (2025-03-09) -#### 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)) +### Bug Fixes -### [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 +* **#2163:** restart closing animation when container height get updated ([4ed9f3c](https://github.com/gorhom/react-native-bottom-sheet/commit/4ed9f3cb542316a984893efa2025ca5384ffe89a)), closes [#2163](https://github.com/gorhom/react-native-bottom-sheet/issues/2163) +* **#2177:** set absolute fill to backdrop default style ([979ba7c](https://github.com/gorhom/react-native-bottom-sheet/commit/979ba7ce0b9d69abfaefd169ee692bf818fa4d0d)), closes [#2177](https://github.com/gorhom/react-native-bottom-sheet/issues/2177) -- 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 +## [5.1.1](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.1.0...v5.1.1) (2025-02-09) -- 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 +### Bug Fixes -- delete debug view from builds ([`7ead04e`](https://github.com/gorhom/react-native-bottom-sheet/commit/7ead04edc1a77cf820adcdadecc912b7791ab14c)) +* **#2043:** handle unnecessary invocation of index side effect ([#2073](https://github.com/gorhom/react-native-bottom-sheet/issues/2073))(inspired by @IslamRustamov) ([2164c02](https://github.com/gorhom/react-native-bottom-sheet/commit/2164c02e63177f9ac69acc05722c85e8d55cd931)), closes [#2043](https://github.com/gorhom/react-native-bottom-sheet/issues/2043) -### [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 +# [5.1.0](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.0.6...v5.1.0) (2025-02-06) -#### 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 +### Bug Fixes -- 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)) +* **#2129:** fixed initial isAnimatedOnMount value ([0850cb8](https://github.com/gorhom/react-native-bottom-sheet/commit/0850cb864819f79189592cb66c2b6d179957ba61)) -### [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 +### Features -- allow handle to drag sheet without effecting the scrollable ([`580b763`](https://github.com/gorhom/react-native-bottom-sheet/commit/580b7632e656403b0797c4e969a35d30f0ec5cb3)) +* added enableBlurKeyboardOnGesture prop to handle blurring keyboard on gesture ([1c31aca](https://github.com/gorhom/react-native-bottom-sheet/commit/1c31acad50a7c171548ea7f4594a4d1d563cf40f)) -### [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 +## [5.0.6](https://github.com/gorhom/react-native-bottom-sheet/compare/v5.0.5...v5.0.6) (2024-11-17) -#### 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..730ef6e23 --- /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": "#000000" + }, + "assetBundlePatterns": ["**/*"], + "ios": { + "supportsTablet": true, + "bundleIdentifier": "dev.gorhom.bottomsheet" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#000000" + }, + "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..8e77dfeb2 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; @@ -24,17 +25,36 @@ const BottomSheetComponent = props => { }; class BottomSheetModal extends React.Component { + // Store mock data passed via present + data = null; + snapToIndex() {} snapToPosition() {} expand() {} collapse() {} - close() {} - forceClose() {} - present() {} - dismiss() {} + close() { + this.data = null; + } + forceClose() { + this.data = null; + } + present(data) { + // Store data passed to present + this.data = data; + // Need to trigger a re-render somehow if component depends on this state, + // but for basic mock, just storing is often enough. + } + dismiss() { + this.data = null; + } render() { - return this.props.children; + const { children: Content } = this.props; + return typeof Content === 'function' ? ( + + ) : ( + Content + ); } } @@ -102,12 +122,97 @@ const useBottomSheetDynamicSnapPoints = () => ({ handleContentLayout: NOOP, }); +const GESTURE_SOURCE = { + UNDETERMINED: 0, + SCROLLABLE: 1, + HANDLE: 2, + CONTENT: 3, +}; + +const SHEET_STATE = { + CLOSED: 0, + OPENED: 1, + EXTENDED: 2, + OVER_EXTENDED: 3, + FILL_PARENT: 4, +}; + +const SCROLLABLE_STATE = { + LOCKED: 0, + UNLOCKED: 1, + UNDETERMINED: 2, +}; + +const SCROLLABLE_TYPE = { + UNDETERMINED: 0, + VIEW: 1, + FLATLIST: 2, + SCROLLVIEW: 3, + SECTIONLIST: 4, + VIRTUALIZEDLIST: 5, +}; + +const ANIMATION_STATE = { + UNDETERMINED: 0, + RUNNING: 1, + STOPPED: 2, + INTERRUPTED: 3, +}; + +const ANIMATION_SOURCE = { + NONE: 0, + MOUNT: 1, + GESTURE: 2, + USER: 3, + CONTAINER_RESIZE: 4, + SNAP_POINT_CHANGE: 5, + KEYBOARD: 6, +}; + +const ANIMATION_METHOD = { + TIMING: 0, + SPRING: 1, +}; + +const KEYBOARD_STATE = { + UNDETERMINED: 0, + SHOWN: 1, + HIDDEN: 2, +}; + +const SNAP_POINT_TYPE = { + PROVIDED: 0, + DYNAMIC: 1, +}; + +const ENUMS = { + GESTURE_SOURCE, + SHEET_STATE, + SCROLLABLE_STATE, + SCROLLABLE_TYPE, + ANIMATION_STATE, + ANIMATION_SOURCE, + ANIMATION_METHOD, + KEYBOARD_STATE, + SNAP_POINT_TYPE, +}; + +const createBottomSheetScrollableComponent = (_type, ScrollableComponent) => { + return ScrollableComponent; +}; + 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, @@ -122,4 +227,7 @@ module.exports = { useBottomSheetInternal, useBottomSheetModalInternal, useBottomSheetDynamicSnapPoints, + + ...ENUMS, + createBottomSheetScrollableComponent, }; diff --git a/package.json b/package.json index 362ad2e9f..fc14bcccf 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,8 @@ { - "name": "@gorhom/bottom-sheet", - "version": "4.4.5", + "name": "@discord/bottom-sheet", + "version": "5.1.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", "files": [ "src", "lib", @@ -15,21 +12,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", @@ -39,38 +37,44 @@ "bootstrap": "yarn install && yarn example" }, "dependencies": { + "@biomejs/biome": "^1.9.4", "@gorhom/portal": "1.0.14", "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": "*", "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", 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..8cd61fb57 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,78 +7,85 @@ import React, { memo, useEffect, } from 'react'; -import { Platform } from 'react-native'; -import invariant from 'invariant'; +import { type Insets, Platform, StyleSheet } from 'react-native'; +import { State } from 'react-native-gesture-handler'; import Animated, { useAnimatedReaction, useSharedValue, - useAnimatedStyle, 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 { + useAnimatedSnapPoints, + useKeyboard, + usePropsValidator, + useReactiveSharedValue, + useScrollable, + useStableCallback, +} from '../../hooks'; +import type { BottomSheetMethods } from '../../types'; import { animate, getKeyboardAnimationConfigs, normalizeSnapPoint, print, } from '../../utilities'; +// import BottomSheetDebugView from '../bottomSheetDebugView'; +import { BottomSheetBackgroundContainer } from '../bottomSheetBackground'; +import { BottomSheetFooterContainer } from '../bottomSheetFooter'; +import BottomSheetGestureHandlersProvider from '../bottomSheetGestureHandlersProvider'; +import { BottomSheetHandleContainer } from '../bottomSheetHandle'; +import { BottomSheetHostingContainer } from '../bottomSheetHostingContainer'; +import { BottomSheetBody } from './BottomSheetBody'; +import { BottomSheetContent } from './BottomSheetContent'; import { - DEFAULT_OVER_DRAG_RESISTANCE_FACTOR, + DEFAULT_ACCESSIBILITY_LABEL, + DEFAULT_ACCESSIBILITY_ROLE, + DEFAULT_ACCESSIBLE, + DEFAULT_ANIMATE_ON_MOUNT, + DEFAULT_DYNAMIC_SIZING, + DEFAULT_ENABLE_BLUR_KEYBOARD_ON_GESTURE, 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_INCLUDE_BOTTOM_OFFSET, 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,15 +103,18 @@ 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, + style, containerStyle: _providedContainerStyle, backgroundStyle: _providedBackgroundStyle, handleStyle: _providedHandleStyle, @@ -120,14 +127,17 @@ const BottomSheetComponent = forwardRef( keyboardBehavior = DEFAULT_KEYBOARD_BEHAVIOR, keyboardBlurBehavior = DEFAULT_KEYBOARD_BLUR_BEHAVIOR, android_keyboardInputMode = DEFAULT_KEYBOARD_INPUT_MODE, + enableBlurKeyboardOnGesture = DEFAULT_ENABLE_BLUR_KEYBOARD_ON_GESTURE, + keyboardIncludeBottomOffset = DEFAULT_KEYBOARD_INCLUDE_BOTTOM_OFFSET, // layout - handleHeight: _providedHandleHeight, containerHeight: _providedContainerHeight, - contentHeight: _providedContentHeight, containerOffset: _providedContainerOffset, topInset = 0, bottomInset = 0, + maxDynamicContentSize, + contentHeight: _providedContentHeight, + handleHeight: _providedHandleHeight, // animated callback shared values animatedPosition: _providedAnimatedPosition, @@ -152,13 +162,34 @@ const BottomSheetComponent = forwardRef( // components handleComponent, - backdropComponent, + backdropComponent: BackdropComponent, backgroundComponent, - footerComponent, - children: Content, + renderFooter, + children, + BodyComponent, + + // 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 +208,30 @@ 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,19 +241,22 @@ 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); + const isAnimatedOnMount = useSharedValue( + !animateOnMount || _providedIndex === -1 + ); const isContentHeightFixed = useSharedValue(false); const isLayoutCalculated = useDerivedValue(() => { let isContainerHeightCalculated = false; @@ -232,14 +273,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 +294,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( @@ -291,8 +331,15 @@ const BottomSheetComponent = forwardRef( animationDuration: keyboardAnimationDuration, animationEasing: keyboardAnimationEasing, shouldHandleKeyboardEvents, - } = useKeyboard(); + } = useKeyboard({ includeBottomOffset: keyboardIncludeBottomOffset }); 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 +350,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 +399,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. */ @@ -386,78 +443,19 @@ const BottomSheetComponent = forwardRef( } return SCROLLABLE_STATE.LOCKED; - }); - // dynamic - const animatedContentHeight = useDerivedValue(() => { - const keyboardHeightInContainer = animatedKeyboardHeightInContainer.value; - const handleHeight = Math.max(0, animatedHandleHeight.value); - let contentHeight = animatedSheetHeight.value - handleHeight; - - if ( - keyboardBehavior === KEYBOARD_BEHAVIOR.extend && - animatedKeyboardState.value === KEYBOARD_STATE.SHOWN - ) { - contentHeight = contentHeight - keyboardHeightInContainer; - } else if ( - keyboardBehavior === KEYBOARD_BEHAVIOR.fillParent && - isInTemporaryPosition.value - ) { - if (animatedKeyboardState.value === KEYBOARD_STATE.SHOWN) { - contentHeight = - animatedContainerHeight.value - - handleHeight - - keyboardHeightInContainer; - } else { - contentHeight = animatedContainerHeight.value - handleHeight; - } - } else if ( - keyboardBehavior === KEYBOARD_BEHAVIOR.interactive && - isInTemporaryPosition.value - ) { - const contentWithKeyboardHeight = - contentHeight + keyboardHeightInContainer; - - if (animatedKeyboardState.value === KEYBOARD_STATE.SHOWN) { - if ( - keyboardHeightInContainer + animatedSheetHeight.value > - animatedContainerHeight.value - ) { - contentHeight = - animatedContainerHeight.value - - keyboardHeightInContainer - - handleHeight; - } - } else if ( - contentWithKeyboardHeight + handleHeight > - animatedContainerHeight.value - ) { - contentHeight = animatedContainerHeight.value - handleHeight; - } else { - contentHeight = contentWithKeyboardHeight; - } - } - - /** - * before the container is measured, `contentHeight` value will be below zero, - * which will lead to freeze the scrollable. - * - * @link (https://github.com/gorhom/react-native-bottom-sheet/issues/470) - */ - return Math.max(contentHeight, 0); }, [ - animatedContainerHeight, - animatedHandleHeight, - animatedKeyboardHeightInContainer, + enableContentPanningGesture, + animatedAnimationState, animatedKeyboardState, - animatedSheetHeight, - isInTemporaryPosition, - keyboardBehavior, + animatedScrollableOverrideState, + animatedSheetState, ]); + // dynamic const animatedIndex = useDerivedValue(() => { const adjustedSnapPoints = animatedSnapPoints.value.slice().reverse(); const adjustedSnapPointsIndexes = animatedSnapPoints.value .slice() - .map((_: any, index: number) => index) + .map((_, index: number) => index) .reverse(); /** @@ -471,7 +469,7 @@ const BottomSheetComponent = forwardRef( animatedPosition.value, adjustedSnapPoints, adjustedSnapPointsIndexes, - Extrapolate.CLAMP + Extrapolation.CLAMP ) : -1; @@ -500,15 +498,259 @@ 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, targetPosition: number, source: ANIMATION_SOURCE) { + if (__DEV__) { + print({ + component: BottomSheet.name, + method: handleOnAnimate.name, + category: 'callback', + params: { + toIndex: targetIndex, + toPosition: targetPosition, + fromIndex: animatedCurrentIndex.value, + fromPosition: animatedPosition.value, + }, + }); + } + + if (!_providedOnAnimate) { + return; + } + + if (targetIndex !== animatedCurrentIndex.value + // there is a race condition when opening and immedately closing the bottom sheet, where + // the animatedCurrentIndex is not updated yet. As we don't want to miss close events we always call the callback for -1 changes: + || targetIndex === -1) { + _providedOnAnimate( + animatedCurrentIndex.value, + targetIndex, + animatedPosition.value, + targetPosition, + source, + ); + } + }, + [_providedOnAnimate, animatedCurrentIndex, animatedPosition] + ); + //#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', + method: 'animateToPositionCompleted', + 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', + method: 'animateToPosition', + params: { + currentPosition: animatedPosition.value, + nextPosition: position, + source, + }, + }); + } + + 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, + * and behavior not extend. + */ + let offset = 0; + if ( + animatedKeyboardState.value === KEYBOARD_STATE.SHOWN && + keyboardBehavior !== KEYBOARD_BEHAVIOR.extend && + position < animatedPosition.value + ) { + offset = animatedKeyboardHeightInContainer.value; + } + + animatedNextPositionIndex.value = animatedSnapPoints.value.indexOf( + position + offset + ); + + /** + * fire `onAnimate` callback + */ + runOnJS(handleOnAnimate)(animatedNextPositionIndex.value, position, source); + + /** + * start animation + */ + animatedPosition.value = animate({ + point: position, + configs: configs || _providedAnimationConfigs, + velocity, + overrideReduceMotion: _providedOverrideReduceMotion, + onComplete: animateToPositionCompleted, + }); + }, + [ + handleOnAnimate, + keyboardBehavior, + _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 +758,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 +774,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 +785,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 +797,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 +816,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,168 +849,157 @@ 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) { + /** + * 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; } - - 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) { + /** + * when evaluating the position while layout is not calculated, then we early exit till it is. + */ + if (!isLayoutCalculated.value) { 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) - ) { + const proposedPosition = getEvaluatedPosition(source); + + /** + * when evaluating the position while the mount animation not been handled, + * then we evaluate on mount use cases. + */ + 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; } - 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 the bottom sheet is animating. */ - animatedAnimationState.value = ANIMATION_STATE.RUNNING; - animatedAnimationSource.value = source; + 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; + } + } /** - * store next position + * when evaluating the position while the bottom sheet is in closed + * position and not animating, we re-set the position to closed position. */ - animatedNextPosition.value = position; - animatedNextPositionIndex.value = - animatedSnapPoints.value.indexOf(position); + if ( + animatedAnimationState.value !== ANIMATION_STATE.RUNNING && + animatedCurrentIndex.value === -1 + ) { + /** + * early exit if reduce motion is enabled and index is out of sync with position. + */ + if ( + reduceMotion && + animatedSnapPoints.value[animatedIndex.value] !== + animatedPosition.value + ) { + return; + } + setToPosition(animatedClosedPosition.value); + return; + } /** - * fire `onAnimate` callback + * when evaluating the position after the container resize, then we + * force the bottom sheet to the proposed position with no + * animation. */ - runOnJS(handleOnAnimate)(position); + if (animatedContainerHeightDidChange.value) { + setToPosition(proposedPosition); + return; + } /** - * force animation configs from parameters, if provided + * we fall back to the proposed position. */ - if (configs !== undefined) { - animatedPosition.value = animate({ - point: position, - configs, - velocity, - onComplete: animateToPositionCompleted, - }); - } else { - /** - * use animationConfigs callback, if provided - */ - animatedPosition.value = animate({ - point: position, - velocity, - configs: _providedAnimationConfigs, - onComplete: animateToPositionCompleted, - }); - } + animateToPosition( + proposedPosition, + source, + undefined, + animationConfigs + ); }, - [handleOnAnimate, _providedAnimationConfigs] + [getEvaluatedPosition, animateToPosition, setToPosition, reduceMotion] ); //#endregion //#region public methods - const handleSnapToIndex = useCallback( - function handleSnapToIndex( - index: number, - animationConfigs?: WithSpringConfig | WithTimingConfig - ) { - const snapPoints = animatedSnapPoints.value; - invariant( - index >= -1 && index <= snapPoints.length - 1, - `'index' was provided but out of the provided snap points range! expected value to be between -1, ${ - snapPoints.length - 1 - }` - ); + const handleSnapToIndex = useStableCallback(function handleSnapToIndex( + index: number, + animationConfigs?: WithSpringConfig | WithTimingConfig + ) { + const snapPoints = animatedSnapPoints.get(); + const isLayoutReady = isLayoutCalculated.get(); + + // early exit if layout is not ready yet. + if (!isLayoutReady) { + return; + } + + invariant( + index >= -1 && index <= snapPoints.length - 1, + `'index' was provided but out of the provided snap points range! expected value to be between -1, ${ + snapPoints.length - 1 + }` + ); + if (__DEV__) { print({ component: BottomSheet.name, method: handleSnapToIndex.name, @@ -748,67 +1007,58 @@ const BottomSheetComponent = forwardRef( index, }, }); + } - const nextPosition = snapPoints[index]; + const nextPosition = snapPoints[index]; - /** - * exit method if : - * - layout is not calculated. - * - already animating to next position. - * - sheet is forced closing. - */ - if ( - !isLayoutCalculated.value || - index === animatedNextPositionIndex.value || - nextPosition === animatedNextPosition.value || - isForcedClosing.value - ) { - return; - } + /** + * exit method if : + * - layout is not calculated. + * - already animating to next position. + * - sheet is forced closing. + */ + if ( + !isLayoutCalculated.value || + index === animatedNextPositionIndex.value || + nextPosition === animatedNextPosition.value || + isForcedClosing.value + ) { + return; + } - /** - * reset temporary position boolean. - */ - isInTemporaryPosition.value = false; + /** + * reset temporary position boolean. + */ + isInTemporaryPosition.value = false; - runOnUI(animateToPosition)( - nextPosition, - ANIMATION_SOURCE.USER, - 0, - animationConfigs - ); - }, - [ - animateToPosition, - isLayoutCalculated, - isInTemporaryPosition, - isForcedClosing, - animatedSnapPoints, - animatedNextPosition, - animatedNextPositionIndex, - ] - ); + runOnUI(animateToPosition)( + nextPosition, + ANIMATION_SOURCE.USER, + 0, + animationConfigs + ); + }); const handleSnapToPosition = useWorkletCallback( function handleSnapToPosition( 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 +1097,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 +1146,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 +1197,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 +1249,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]; @@ -1053,6 +1315,7 @@ const BottomSheetComponent = forwardRef( const internalContextVariables = useMemo( () => ({ enableContentPanningGesture, + enableDynamicSizing, overDragResistanceFactor, enableOverDrag, enablePanDownToClose, @@ -1066,6 +1329,7 @@ const BottomSheetComponent = forwardRef( animatedScrollableType, animatedIndex, animatedPosition, + animatedSheetHeight, animatedContentHeight, animatedClosedPosition, animatedHandleHeight, @@ -1086,6 +1350,7 @@ const BottomSheetComponent = forwardRef( activeOffsetY: _providedActiveOffsetY, failOffsetX: _providedFailOffsetX, failOffsetY: _providedFailOffsetY, + enableBlurKeyboardOnGesture, animateToPosition, stopAnimation, setScrollableRef, @@ -1095,6 +1360,7 @@ const BottomSheetComponent = forwardRef( animatedIndex, animatedPosition, animatedContentHeight, + animatedSheetHeight, animatedScrollableType, animatedContentGestureState, animatedHandleGestureState, @@ -1120,6 +1386,8 @@ const BottomSheetComponent = forwardRef( overDragResistanceFactor, enableOverDrag, enablePanDownToClose, + enableDynamicSizing, + enableBlurKeyboardOnGesture, _providedSimultaneousHandlers, _providedWaitFor, _providedActiveOffsetX, @@ -1156,198 +1424,82 @@ const BottomSheetComponent = forwardRef( ); //#endregion - //#region styles - const containerAnimatedStyle = useAnimatedStyle( - () => ({ - opacity: - Platform.OS === 'android' && animatedIndex.value === -1 ? 0 : 1, - transform: [ - { - translateY: animatedPosition.value, - }, - ], - }), - [animatedPosition, animatedIndex] - ); - const containerStyle = useMemo( - () => [_providedStyle, styles.container, containerAnimatedStyle], - [_providedStyle, containerAnimatedStyle] - ); - const contentContainerAnimatedStyle = useAnimatedStyle(() => { - /** - * if content height was provided, then we skip setting - * calculated height. - */ - if (_providedContentHeight) { - return {}; - } - - return { - height: animate({ - point: animatedContentHeight.value, - configs: _providedAnimationConfigs, - }), - }; - }, [animatedContentHeight, _providedContentHeight]); - const contentContainerStyle = useMemo( - () => [styles.contentContainer, contentContainerAnimatedStyle], - [contentContainerAnimatedStyle] - ); - /** - * added safe area to prevent the sheet from floating above - * the bottom of the screen, when sheet being over dragged or - * when the sheet is resized. - */ - const contentMaskContainerAnimatedStyle = useAnimatedStyle(() => { - if (detached) { - return { - overflow: 'visible', - }; - } - return { - paddingBottom: animatedContainerHeight.value, - }; - }, [detached]); - const contentMaskContainerStyle = useMemo( - () => [styles.contentMaskContainer, contentMaskContainerAnimatedStyle], - [contentMaskContainerAnimatedStyle] - ); - //#endregion - //#region effects - /** - * React to `isLayoutCalculated` change, to insure that the sheet will - * appears/mounts only when all layout is been calculated. - * - * @alias OnMount - */ useAnimatedReaction( - () => isLayoutCalculated.value, - _isLayoutCalculated => { - /** - * exit method if: - * - layout is not calculated yet. - * - already did animate on mount. - */ - if (!_isLayoutCalculated || isAnimatedOnMount.value) { + () => animatedContainerHeight.value, + (result, previous) => { + if (result === INITIAL_CONTAINER_HEIGHT) { 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, - }, - }); + animatedContainerHeightDidChange.value = result !== previous; /** - * here we exit method early because the next position - * is out of the screen, this happens when `snapPoints` - * still being calculated. + * When user close the bottom sheet while the keyboard open on Android with + * software keyboard layout mode set to resize, the close position would be + * set to the container height - the keyboard height, and when the keyboard + * closes, the container height and here we restart the animation again. + * + * [read more](https://github.com/gorhom/react-native-bottom-sheet/issues/2163) */ if ( - nextPosition === INITIAL_POSITION || - nextPosition === animatedClosedPosition.value + animatedAnimationState.value === ANIMATION_STATE.RUNNING && + animatedAnimationSource.value === ANIMATION_SOURCE.GESTURE && + animatedNextPositionIndex.value === -1 ) { - isAnimatedOnMount.value = true; - animatedCurrentIndex.value = _providedIndex; - return; - } - - if (animateOnMount) { - animateToPosition(nextPosition, ANIMATION_SOURCE.MOUNT); - } else { - animatedPosition.value = nextPosition; + animateToPosition( + animatedClosedPosition.value, + ANIMATION_SOURCE.GESTURE + ); } - isAnimatedOnMount.value = true; - }, - [_providedIndex, animateOnMount] + } ); /** - * React to `snapPoints` change, to insure that the sheet position reflect + * Reaction to the `snapPoints` change, to insure that the sheet position reflect * to the current point correctly. * * @alias OnSnapPointsChange */ useAnimatedReaction( - () => ({ - snapPoints: animatedSnapPoints.value, - containerHeight: animatedContainerHeight.value, - }), - (result, _previousResult) => { - const { snapPoints, containerHeight } = result; - const _previousSnapPoints = _previousResult?.snapPoints; - const _previousContainerHeight = _previousResult?.containerHeight; - + () => animatedSnapPoints.value, + (result, previous) => { + /** + * if values did not change, and did handle on mount animation + * then we early exit the method. + */ if ( - JSON.stringify(snapPoints) === JSON.stringify(_previousSnapPoints) || - !isLayoutCalculated.value || - !isAnimatedOnMount.value || - containerHeight <= 0 + JSON.stringify(result) === JSON.stringify(previous) && + isAnimatedOnMount.value ) { return; } - runOnJS(print)({ - component: BottomSheet.name, - method: 'useAnimatedReaction::OnSnapPointChange', - params: { - snapPoints, - }, - }); - - let nextPosition; - let animationConfig; - let animationSource = ANIMATION_SOURCE.SNAP_POINT_CHANGE; - /** - * if snap points changed while sheet is animating, then - * we stop the animation and animate to the updated point. + * if layout is not calculated yet, then we exit the method. */ - if ( - animatedAnimationState.value === ANIMATION_STATE.RUNNING && - animatedNextPositionIndex.value !== animatedCurrentIndex.value - ) { - 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]; + if (!isLayoutCalculated.value) { + return; + } - /** - * if snap points changes because of the container height change, - * then we skip the snap animation by setting the duration to 0. - */ - if (containerHeight !== _previousContainerHeight) { - animationSource = ANIMATION_SOURCE.CONTAINER_RESIZE; - animationConfig = { - duration: 0, - }; - } + if (__DEV__) { + runOnJS(print)({ + component: 'BottomSheet', + method: 'useAnimatedReaction::OnSnapPointChange', + category: 'effect', + params: { + result, + }, + }); } - animateToPosition(nextPosition, animationSource, 0, animationConfig); - } + + evaluatePosition(ANIMATION_SOURCE.SNAP_POINT_CHANGE); + }, + [isLayoutCalculated, animatedSnapPoints] ); /** - * React to keyboard appearance state. + * Reaction to the keyboard state change. * * @alias OnKeyboardStateChange */ @@ -1361,72 +1513,103 @@ const BottomSheetComponent = forwardRef( const _previousKeyboardState = _previousResult?._keyboardState; const _previousKeyboardHeight = _previousResult?._keyboardHeight; + /** + * if keyboard state is equal to the previous state, then exit the method + */ + if ( + _keyboardState === _previousKeyboardState && + _keyboardHeight === _previousKeyboardHeight + ) { + return; + } + + /** + * if state is undetermined, then we early exit. + */ + if (_keyboardState === KEYBOARD_STATE.UNDETERMINED) { + return; + } + + /** + * if keyboard is hidden by customer gesture, then we early exit. + */ + if ( + _keyboardState === KEYBOARD_STATE.HIDDEN && + animatedAnimationState.value === ANIMATION_STATE.RUNNING && + animatedAnimationSource.value === ANIMATION_SOURCE.GESTURE + ) { + 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 = $modal - ? Math.abs( - _keyboardHeight - - Math.abs(bottomInset - animatedContainerOffset.value.bottom) - ) - : Math.abs(_keyboardHeight - animatedContainerOffset.value.bottom); + 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 + ) { + animatedKeyboardHeightInContainer.value = 0; + if (keyboardBehavior === KEYBOARD_BEHAVIOR.interactive) { + return; + } + } + + /** + * if user is interacting with sheet, then exit the method + */ 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 +1618,7 @@ const BottomSheetComponent = forwardRef( keyboardBlurBehavior, android_keyboardInputMode, animatedContainerOffset, - getNextPosition, + getEvaluatedPosition, ] ); @@ -1448,7 +1631,8 @@ const BottomSheetComponent = forwardRef( if (_providedAnimatedPosition) { _providedAnimatedPosition.value = _animatedPosition + topInset; } - } + }, + [] ); /** @@ -1460,7 +1644,8 @@ const BottomSheetComponent = forwardRef( if (_providedAnimatedIndex) { _providedAnimatedIndex.value = _animatedIndex; } - } + }, + [] ); /** @@ -1478,6 +1663,7 @@ const BottomSheetComponent = forwardRef( }), ({ _animatedIndex, + _animatedPosition, _animationState, _contentGestureState, _handleGestureState, @@ -1489,6 +1675,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 +1712,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] ); /** @@ -1554,40 +1774,30 @@ const BottomSheetComponent = forwardRef( * @alias onIndexChange */ useEffect(() => { - if (isAnimatedOnMount.value) { - handleSnapToIndex(_providedIndex); + // early exit, if animate on mount is set and it did not animate yet. + if (animateOnMount && !isAnimatedOnMount.value) { + return; } - }, [ - _providedIndex, - animatedCurrentIndex, - isAnimatedOnMount, - handleSnapToIndex, - ]); + + handleSnapToIndex(_providedIndex); + }, [animateOnMount, _providedIndex, isAnimatedOnMount, handleSnapToIndex]); //#endregion // render - print({ - component: BottomSheet.name, - method: 'render', - params: { - animatedSnapPoints: animatedSnapPoints.value, - animatedCurrentIndex: animatedCurrentIndex.value, - providedIndex: _providedIndex, - }, - }); return ( - - + ) : null} + ( detached={detached} style={_providedContainerStyle} > - - - + {backgroundComponent === null ? null : ( + + )} + - - {typeof Content === 'function' ? : Content} - - {footerComponent && ( - - )} - - - - + detached={detached} + > + {children} + {renderFooter ? ( + + ) : null} + + {handleComponent !== null ? ( + + ) : null} + {/* */} - + diff --git a/src/components/bottomSheet/BottomSheetBody.tsx b/src/components/bottomSheet/BottomSheetBody.tsx new file mode 100644 index 000000000..daa06523e --- /dev/null +++ b/src/components/bottomSheet/BottomSheetBody.tsx @@ -0,0 +1,49 @@ +import React, { type ComponentProps, memo, useMemo } from 'react'; +import { Platform } from 'react-native'; +import Animated, { useAnimatedStyle } from 'react-native-reanimated'; +import { useBottomSheetInternal } from '../../hooks'; +import type { BottomSheetProps } from '../bottomSheet/types'; +import { styles } from './styles'; + +export type BottomSheetBodyProps = { + style?: BottomSheetProps['style']; + children?: React.ReactNode; + BodyComponent?: React.ComponentType>; +}; + +function BottomSheetBodyComponent({ + style, + children, + BodyComponent = Animated.View, +}: BottomSheetBodyProps) { + //#region hooks + const { animatedIndex, animatedPosition } = useBottomSheetInternal(); + //#endregion + + //#region styles + const containerAnimatedStyle = useAnimatedStyle( + () => ({ + opacity: Platform.OS === 'android' && animatedIndex.get() === -1 ? 0 : 1, + transform: [ + { + translateY: animatedPosition.get(), + }, + ], + }), + [animatedPosition, animatedIndex] + ); + const containerStyle = useMemo( + () => [style, styles.container, containerAnimatedStyle], + [style, containerAnimatedStyle] + ); + //#endregion + + return ( + + {children} + + ); +} + +export const BottomSheetBody = memo(BottomSheetBodyComponent); +BottomSheetBody.displayName = 'BottomSheetBody'; diff --git a/src/components/bottomSheet/BottomSheetContent.tsx b/src/components/bottomSheet/BottomSheetContent.tsx new file mode 100644 index 000000000..1dfaa801b --- /dev/null +++ b/src/components/bottomSheet/BottomSheetContent.tsx @@ -0,0 +1,250 @@ +import React, { memo, useMemo } from 'react'; +import type { ViewProps, ViewStyle } from 'react-native'; +import Animated, { + type AnimatedStyle, + useAnimatedStyle, + useDerivedValue, +} from 'react-native-reanimated'; +import { KEYBOARD_BEHAVIOR, KEYBOARD_STATE } from '../../constants'; +import { useBottomSheetInternal } from '../../hooks'; +import type { NullableAccessibilityProps } from '../../types'; +import { animate } from '../../utilities'; +import BottomSheetDraggableView from '../bottomSheetDraggableView'; +import { INITIAL_CONTAINER_HEIGHT } from './constants'; +import type { BottomSheetProps } from './types'; + +type BottomSheetContent = { + style?: AnimatedStyle; +} & Pick< + BottomSheetProps, + | 'children' + | 'detached' + | 'animationConfigs' + | 'overrideReduceMotion' + | 'keyboardBehavior' +> & + NullableAccessibilityProps & + ViewProps; + +function BottomSheetContentComponent({ + detached, + animationConfigs, + overrideReduceMotion, + keyboardBehavior, + accessible, + accessibilityLabel, + accessibilityHint, + accessibilityRole, + children, +}: BottomSheetContent) { + //#region hooks + const { + enableDynamicSizing, + overDragResistanceFactor, + enableContentPanningGesture, + animatedPosition, + animatedHandleHeight, + animatedHighestSnapPoint, + animatedContainerHeight, + animatedContentHeight, + animatedSheetHeight, + animatedKeyboardState, + animatedKeyboardHeightInContainer, + isInTemporaryPosition, + } = useBottomSheetInternal(); + //#endregion + + //#region variables + const animatedContentHeightMax = useDerivedValue(() => { + /** + * if container height is not yet calculated, then we exit the method + */ + if (animatedContainerHeight.get() === INITIAL_CONTAINER_HEIGHT) { + return 0; + } + + const keyboardState = animatedKeyboardState.get(); + const keyboardHeightInContainer = animatedKeyboardHeightInContainer.get(); + const handleHeight = Math.max(0, animatedHandleHeight.get()); + const containerHeight = animatedContainerHeight.get(); + let contentHeight = animatedSheetHeight.get() - handleHeight; + + switch (keyboardBehavior) { + case KEYBOARD_BEHAVIOR.extend: + if (keyboardState === KEYBOARD_STATE.SHOWN) { + contentHeight = contentHeight - keyboardHeightInContainer; + } + break; + + case KEYBOARD_BEHAVIOR.fillParent: + if (!isInTemporaryPosition.get()) { + break; + } + + if (keyboardState === KEYBOARD_STATE.SHOWN) { + contentHeight = + containerHeight - handleHeight - keyboardHeightInContainer; + } else { + contentHeight = containerHeight - handleHeight; + } + break; + + case KEYBOARD_BEHAVIOR.interactive: { + if (!isInTemporaryPosition.get()) { + break; + } + const contentWithKeyboardHeight = + contentHeight + keyboardHeightInContainer; + + if (keyboardState === KEYBOARD_STATE.SHOWN) { + if ( + keyboardHeightInContainer + animatedSheetHeight.get() > + containerHeight + ) { + contentHeight = + containerHeight - keyboardHeightInContainer - handleHeight; + } + } else if (contentWithKeyboardHeight + handleHeight > containerHeight) { + contentHeight = containerHeight - handleHeight; + } else { + contentHeight = contentWithKeyboardHeight; + } + break; + } + } + + /** + * before the container is measured, `contentHeight` value will be below zero, + * which will lead to freeze the scrollable. + * + * @link (https://github.com/gorhom/react-native-bottom-sheet/issues/470) + */ + return Math.max(contentHeight, 0); + }, [ + animatedContainerHeight, + animatedHandleHeight, + animatedKeyboardHeightInContainer, + animatedKeyboardState, + animatedSheetHeight, + isInTemporaryPosition, + keyboardBehavior, + ]); + const animatedPaddingBottom = useDerivedValue(() => { + const containerHeight = animatedContainerHeight.get(); + /** + * if container height is not yet calculated, then we exit the method + */ + if (containerHeight === INITIAL_CONTAINER_HEIGHT) { + return 0; + } + + const highestSnapPoint = Math.max( + animatedHighestSnapPoint.get(), + animatedPosition.get() + ); + /** + * added safe area to prevent the sheet from floating above + * the bottom of the screen, when sheet being over dragged or + * when the sheet is resized. + */ + const overDragSafePaddingBottom = + Math.sqrt(highestSnapPoint - containerHeight * -1) * + overDragResistanceFactor; + + let paddingBottom = overDragSafePaddingBottom; + + /** + * if keyboard is open, then we try to add padding to prevent content + * from being covered by the keyboard. + */ + if (animatedKeyboardState.get() === KEYBOARD_STATE.SHOWN) { + paddingBottom = + overDragSafePaddingBottom + animatedKeyboardHeightInContainer.get(); + } + + return paddingBottom; + }, [ + overDragResistanceFactor, + animatedPosition, + animatedContainerHeight, + animatedHighestSnapPoint, + animatedKeyboardState, + animatedKeyboardHeightInContainer, + ]); + //#endregion + + //#region styles + const contentMaskContainerAnimatedStyle = useAnimatedStyle(() => { + /** + * if container height is not yet calculated, then we exit the method + */ + if (animatedContainerHeight.get() === INITIAL_CONTAINER_HEIGHT) { + return {}; + } + + /** + * if dynamic sizing is enabled, and content height + * is still not set, then we exit method. + */ + if ( + enableDynamicSizing && + animatedContentHeight.get() === INITIAL_CONTAINER_HEIGHT + ) { + return {}; + } + + const paddingBottom = detached ? 0 : animatedPaddingBottom.get(); + + return { + paddingBottom: animate({ + point: paddingBottom, + configs: animationConfigs, + overrideReduceMotion, + }), + height: animate({ + point: animatedContentHeightMax.get() + paddingBottom, + configs: animationConfigs, + overrideReduceMotion, + }), + }; + }, [ + overDragResistanceFactor, + enableDynamicSizing, + detached, + animationConfigs, + overrideReduceMotion, + animatedContentHeight, + animatedContentHeightMax, + animatedContainerHeight, + ]); + const contentContainerStyle = useMemo( + () => [ + detached + ? { overflow: 'visible' as const } + : { overflow: 'hidden' as const }, + contentMaskContainerAnimatedStyle, + ], + [contentMaskContainerAnimatedStyle, detached] + ); + //#endregion + + //#region render + const DraggableView = enableContentPanningGesture + ? BottomSheetDraggableView + : Animated.View; + return ( + + {children} + + ); + //#endregion +} + +export const BottomSheetContent = memo(BottomSheetContentComponent); +BottomSheetContent.displayName = 'BottomSheetContent'; diff --git a/src/components/bottomSheet/constants.ts b/src/components/bottomSheet/constants.ts index a08599191..fedd5c4a9 100644 --- a/src/components/bottomSheet/constants.ts +++ b/src/components/bottomSheet/constants.ts @@ -13,11 +13,14 @@ 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; const DEFAULT_KEYBOARD_BLUR_BEHAVIOR = KEYBOARD_BLUR_BEHAVIOR.none; const DEFAULT_KEYBOARD_INPUT_MODE = KEYBOARD_INPUT_MODE.adjustPan; +const DEFAULT_ENABLE_BLUR_KEYBOARD_ON_GESTURE = false; +const DEFAULT_KEYBOARD_INCLUDE_BOTTOM_OFFSET = false; // initial values const INITIAL_VALUE = Number.NEGATIVE_INFINITY; @@ -32,6 +35,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,11 +47,14 @@ 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, DEFAULT_KEYBOARD_BLUR_BEHAVIOR, DEFAULT_KEYBOARD_INPUT_MODE, + DEFAULT_ENABLE_BLUR_KEYBOARD_ON_GESTURE, + DEFAULT_KEYBOARD_INCLUDE_BOTTOM_OFFSET, // layout INITIAL_POSITION, INITIAL_CONTAINER_HEIGHT, @@ -51,4 +62,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..58458f692 100644 --- a/src/components/bottomSheet/styles.ts +++ b/src/components/bottomSheet/styles.ts @@ -8,8 +8,4 @@ export const styles = StyleSheet.create({ left: 0, right: 0, }, - contentContainer: {}, - contentMaskContainer: { - overflow: 'hidden', - }, }); diff --git a/src/components/bottomSheet/types.d.ts b/src/components/bottomSheet/types.d.ts index c489981e1..c96ba9ade 100644 --- a/src/components/bottomSheet/types.d.ts +++ b/src/components/bottomSheet/types.d.ts @@ -1,37 +1,35 @@ import type React from 'react'; -import type { ViewStyle, Insets, StyleProp } from 'react-native'; +import type { Insets, StyleProp, View, 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'; +import type { BottomSheetBodyProps } from './BottomSheetBody'; export interface BottomSheetProps extends BottomSheetAnimationConfigs, - Partial< - Pick< - PanGestureHandlerProps, - | 'activeOffsetY' - | 'activeOffsetX' - | 'failOffsetY' - | 'failOffsetX' - | 'waitFor' - | 'simultaneousHandlers' - > - > { + Partial, + Omit, + Pick { //#region configuration /** * Initial snap point index, provide `-1` to initiate bottom sheet in closed state. @@ -42,13 +40,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 +91,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 +122,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 +132,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 +155,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 @@ -152,6 +181,11 @@ export interface BottomSheetProps * - `restore`: restore sheet position. */ keyboardBlurBehavior?: keyof typeof KEYBOARD_BLUR_BEHAVIOR; + /** + * Enable blurring the keyboard when user start to drag the bottom sheet. + * @default false + */ + enableBlurKeyboardOnGesture?: boolean; /** * Defines keyboard input mode for Android only. * @link {https://developer.android.com/guide/topics/manifest/activity-element#wsoft} @@ -160,6 +194,12 @@ export interface BottomSheetProps */ android_keyboardInputMode?: keyof typeof KEYBOARD_INPUT_MODE; + /** + * Determines the bottom offset of the keyboard (e.g. nav bar) and includes it in the keyboard height. + * @default false + */ + keyboardIncludeBottomOffset?: boolean; + //#endregion //#region styles @@ -240,7 +280,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 +290,15 @@ export interface BottomSheetProps /** * Callback when the sheet about to animate to a new position. * - * @type (fromIndex: number, toIndex: number) => void; - */ - onAnimate?: (fromIndex: number, toIndex: number) => void; + * @type (fromIndex: number, toIndex: number, fromPosition: number, toPosition: number) => void; + */ + onAnimate?: ( + fromIndex: number, + toIndex: number, + fromPosition: number, + toPosition: number, + source: ANIMATION_SOURCE + ) => void; //#endregion //#region components @@ -262,13 +308,14 @@ export interface BottomSheetProps * @type React.FC\ */ handleComponent?: React.FC | null; + /** * Component to be placed as a sheet backdrop. * @see {BottomSheetBackdropProps} * @type React.FC\ - * @default null + * @default undefined */ - backdropComponent?: React.FC | null; + backdropComponent?: React.FC; /** * Component to be placed as a background. * @see {BottomSheetBackgroundProps} @@ -276,16 +323,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 +360,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..5fe8c9d02 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,15 @@ const BottomSheetBackdropComponent = ({ onPress, style, children, + ViewComponent: AnimatedViewComponent = Animated.View, + 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,36 +80,36 @@ 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 + ), + }), + [animatedIndex, appearsOnIndex, disappearsOnIndex, opacity] + ); const containerStyle = useMemo( - () => [styles.container, style, containerAnimatedStyle], + () => [styles.backdrop, style, containerAnimatedStyle], [style, containerAnimatedStyle] ); //#endregion @@ -112,31 +125,42 @@ 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 + const AnimatedView = ( + + {children} + + ); + return pressBehavior !== 'none' ? ( - - - {children} - - + {AnimatedView} ) : ( - - {children} - + AnimatedView ); }; -const BottomSheetBackdrop = memo(BottomSheetBackdropComponent); +export const BottomSheetBackdrop = memo(BottomSheetBackdropComponent); BottomSheetBackdrop.displayName = 'BottomSheetBackdrop'; - -export default BottomSheetBackdrop; 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/index.ts b/src/components/bottomSheetBackdrop/index.ts index 2ee0ef56c..3ed54ddf1 100644 --- a/src/components/bottomSheetBackdrop/index.ts +++ b/src/components/bottomSheetBackdrop/index.ts @@ -1,2 +1,2 @@ -export { default } from './BottomSheetBackdrop'; +export { BottomSheetBackdrop } from './BottomSheetBackdrop'; export type { BottomSheetBackdropProps } from './types'; diff --git a/src/components/bottomSheetBackdrop/styles.ts b/src/components/bottomSheetBackdrop/styles.ts index 1e1e596c4..6151fe892 100644 --- a/src/components/bottomSheetBackdrop/styles.ts +++ b/src/components/bottomSheetBackdrop/styles.ts @@ -1,7 +1,8 @@ import { StyleSheet } from 'react-native'; export const styles = StyleSheet.create({ - container: { + backdrop: { + ...StyleSheet.absoluteFillObject, backgroundColor: 'black', }, }); diff --git a/src/components/bottomSheetBackdrop/types.d.ts b/src/components/bottomSheetBackdrop/types.d.ts index 6a33b9a73..5c200c66c 100644 --- a/src/components/bottomSheetBackdrop/types.d.ts +++ b/src/components/bottomSheetBackdrop/types.d.ts @@ -1,6 +1,11 @@ -import { ReactNode } from 'react'; +import type { ComponentType, ReactNode } from 'react'; import type { ViewProps } from 'react-native'; -import type { BottomSheetVariables } from '../../types'; +import type { AnimatedProps } from 'react-native-reanimated'; +import type { + BottomSheetVariables, + NullableAccessibilityProps, +} from '../../types'; +import type { BottomSheetProps } from '../bottomSheet/types'; export interface BottomSheetBackdropProps extends Pick, @@ -9,7 +14,8 @@ export interface BottomSheetBackdropProps export type BackdropPressBehavior = 'none' | 'close' | 'collapse' | number; export interface BottomSheetDefaultBackdropProps - extends BottomSheetBackdropProps { + extends BottomSheetBackdropProps, + NullableAccessibilityProps { /** * Backdrop opacity. * @type number @@ -50,4 +56,10 @@ export interface BottomSheetDefaultBackdropProps * Child component that will be rendered on backdrop. */ children?: ReactNode | ReactNode[]; + + /** + * Optional component that will be used as a backdrop. + * Default is `Reanimated.View`. + */ + ViewComponent?: ComponentType>; } diff --git a/src/components/bottomSheetBackdropContainer/BottomSheetBackdropContainer.tsx b/src/components/bottomSheetBackdropContainer/BottomSheetBackdropContainer.tsx deleted file mode 100644 index 50daa6f57..000000000 --- a/src/components/bottomSheetBackdropContainer/BottomSheetBackdropContainer.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { memo } from 'react'; -import type { BottomSheetBackdropContainerProps } from './types'; -import { styles } from './styles'; - -const BottomSheetBackdropContainerComponent = ({ - animatedIndex, - animatedPosition, - backdropComponent: BackdropComponent, -}: BottomSheetBackdropContainerProps) => { - return BackdropComponent ? ( - - ) : null; -}; - -const BottomSheetBackdropContainer = memo( - BottomSheetBackdropContainerComponent -); -BottomSheetBackdropContainer.displayName = 'BottomSheetBackdropContainer'; - -export default BottomSheetBackdropContainer; diff --git a/src/components/bottomSheetBackdropContainer/index.ts b/src/components/bottomSheetBackdropContainer/index.ts deleted file mode 100644 index 3db6d12f8..000000000 --- a/src/components/bottomSheetBackdropContainer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './BottomSheetBackdropContainer'; diff --git a/src/components/bottomSheetBackdropContainer/types.d.ts b/src/components/bottomSheetBackdropContainer/types.d.ts deleted file mode 100644 index de06232d1..000000000 --- a/src/components/bottomSheetBackdropContainer/types.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { BottomSheetProps } from '../bottomSheet'; -import type { BottomSheetBackdropProps } from '../bottomSheetBackdrop'; - -export interface BottomSheetBackdropContainerProps - extends Pick, - BottomSheetBackdropProps {} diff --git a/src/components/bottomSheetBackground/BottomSheetBackground.tsx b/src/components/bottomSheetBackground/BottomSheetBackground.tsx index 71ce0c37a..11e7306b8 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, @@ -12,11 +12,9 @@ const BottomSheetBackgroundComponent = ({ accessible={true} accessibilityRole="adjustable" accessibilityLabel="Bottom Sheet" - style={[styles.container, style]} + style={[styles.background, style]} /> ); -const BottomSheetBackground = memo(BottomSheetBackgroundComponent); +export const BottomSheetBackground = memo(BottomSheetBackgroundComponent); BottomSheetBackground.displayName = 'BottomSheetBackground'; - -export default BottomSheetBackground; diff --git a/src/components/bottomSheetBackgroundContainer/BottomSheetBackgroundContainer.tsx b/src/components/bottomSheetBackground/BottomSheetBackgroundContainer.tsx similarity index 76% rename from src/components/bottomSheetBackgroundContainer/BottomSheetBackgroundContainer.tsx rename to src/components/bottomSheetBackground/BottomSheetBackgroundContainer.tsx index 80a859ef6..b8d4ed100 100644 --- a/src/components/bottomSheetBackgroundContainer/BottomSheetBackgroundContainer.tsx +++ b/src/components/bottomSheetBackground/BottomSheetBackgroundContainer.tsx @@ -1,8 +1,8 @@ import React, { memo, useMemo } from 'react'; -import BottomSheetBackground from '../bottomSheetBackground'; -import type { BottomSheetBackgroundContainerProps } from './types'; -import { styles } from './styles'; import { StyleSheet } from 'react-native'; +import { BottomSheetBackground } from './BottomSheetBackground'; +import { styles } from './styles'; +import type { BottomSheetBackgroundContainerProps } from './types'; const BottomSheetBackgroundContainerComponent = ({ animatedIndex, @@ -10,15 +10,16 @@ const BottomSheetBackgroundContainerComponent = ({ backgroundComponent: _providedBackgroundComponent, backgroundStyle: _providedBackgroundStyle, }: BottomSheetBackgroundContainerProps) => { - const BackgroundComponent = - _providedBackgroundComponent || BottomSheetBackground; - + //#region style const backgroundStyle = useMemo( () => StyleSheet.flatten([styles.container, _providedBackgroundStyle]), [_providedBackgroundStyle] ); + //#endregion - return _providedBackgroundComponent === null ? null : ( + const BackgroundComponent = + _providedBackgroundComponent ?? BottomSheetBackground; + return ( , BottomSheetVariables {} + +export type BottomSheetBackgroundContainerProps = Pick< + BottomSheetProps, + 'backgroundComponent' | 'backgroundStyle' +> & + BottomSheetBackgroundProps; diff --git a/src/components/bottomSheetBackgroundContainer/index.ts b/src/components/bottomSheetBackgroundContainer/index.ts deleted file mode 100644 index 929b5e06f..000000000 --- a/src/components/bottomSheetBackgroundContainer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './BottomSheetBackgroundContainer'; diff --git a/src/components/bottomSheetBackgroundContainer/styles.ts b/src/components/bottomSheetBackgroundContainer/styles.ts deleted file mode 100644 index 7a3253f94..000000000 --- a/src/components/bottomSheetBackgroundContainer/styles.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { StyleSheet } from 'react-native'; - -export const styles = StyleSheet.create({ - container: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - }, -}); diff --git a/src/components/bottomSheetBackgroundContainer/types.d.ts b/src/components/bottomSheetBackgroundContainer/types.d.ts deleted file mode 100644 index 3b0978311..000000000 --- a/src/components/bottomSheetBackgroundContainer/types.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { BottomSheetProps } from '../bottomSheet'; -import type { BottomSheetBackgroundProps } from '../bottomSheetBackground'; - -export interface BottomSheetBackgroundContainerProps - extends Pick, - BottomSheetBackgroundProps {} diff --git a/src/components/bottomSheetContainer/BottomSheetContainer.tsx b/src/components/bottomSheetContainer/BottomSheetContainer.tsx deleted file mode 100644 index 9d42ef185..000000000 --- a/src/components/bottomSheetContainer/BottomSheetContainer.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { memo, useCallback, useMemo, useRef } from 'react'; -import { - LayoutChangeEvent, - StatusBar, - StyleProp, - View, - ViewStyle, -} from 'react-native'; -import { WINDOW_HEIGHT } from '../../constants'; -import { print } from '../../utilities'; -import { styles } from './styles'; -import type { BottomSheetContainerProps } from './types'; - -function BottomSheetContainerComponent({ - containerHeight, - containerOffset, - topInset = 0, - bottomInset = 0, - shouldCalculateHeight = true, - detached, - style, - children, -}: BottomSheetContainerProps) { - const containerRef = useRef(null); - //#region styles - const containerStyle = useMemo>( - () => [ - style, - styles.container, - { - top: topInset, - bottom: bottomInset, - overflow: detached ? 'visible' : 'hidden', - }, - ], - [style, detached, topInset, bottomInset] - ); - //#endregion - - //#region callbacks - const handleContainerLayout = useCallback( - function handleContainerLayout({ - nativeEvent: { - layout: { height }, - }, - }: LayoutChangeEvent) { - containerHeight.value = height; - - containerRef.current?.measure( - (_x, _y, _width, _height, _pageX, pageY) => { - containerOffset.value = { - top: pageY, - left: 0, - right: 0, - bottom: Math.max( - 0, - WINDOW_HEIGHT - (pageY + height + (StatusBar.currentHeight ?? 0)) - ), - }; - } - ); - - print({ - component: BottomSheetContainer.displayName, - method: 'handleContainerLayout', - params: { - height, - }, - }); - }, - [containerHeight, containerOffset, containerRef] - ); - //#endregion - - //#region render - return ( - - ); - //#endregion -} - -const BottomSheetContainer = memo(BottomSheetContainerComponent); -BottomSheetContainer.displayName = 'BottomSheetContainer'; - -export default BottomSheetContainer; diff --git a/src/components/bottomSheetContainer/index.ts b/src/components/bottomSheetContainer/index.ts deleted file mode 100644 index ff4646d1a..000000000 --- a/src/components/bottomSheetContainer/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './BottomSheetContainer'; -export type { BottomSheetContainerProps } from './types'; diff --git a/src/components/bottomSheetContainer/styles.web.ts b/src/components/bottomSheetContainer/styles.web.ts deleted file mode 100644 index 086ed0d0c..000000000 --- a/src/components/bottomSheetContainer/styles.web.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { StyleSheet } from 'react-native'; - -export const styles = StyleSheet.create({ - container: { - // @ts-ignore - position: 'fixed', - left: 0, - right: 0, - bottom: 0, - top: 0, - }, -}); 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..bf430fd64 100644 --- a/src/components/bottomSheetFooter/BottomSheetFooter.tsx +++ b/src/components/bottomSheetFooter/BottomSheetFooter.tsx @@ -1,10 +1,15 @@ -import React, { memo, useCallback, useMemo } from 'react'; -import { LayoutChangeEvent } from 'react-native'; +import React, { memo, useCallback, useMemo, useRef } from 'react'; +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 { + type BoundingClientRect, + useBottomSheetInternal, + useBoundingClientRect, +} from '../../hooks'; +import { print } from '../../utilities'; import { styles } from './styles'; +import type { BottomSheetDefaultFooterProps } from './types'; function BottomSheetFooterComponent({ animatedFooterPosition, @@ -12,6 +17,10 @@ function BottomSheetFooterComponent({ style, children, }: BottomSheetDefaultFooterProps) { + //#region refs + const ref = useRef(null); + //#endregion + //#region hooks const { animatedFooterHeight, animatedKeyboardState } = useBottomSheetInternal(); @@ -19,12 +28,12 @@ function BottomSheetFooterComponent({ //#region styles const containerAnimatedStyle = useAnimatedStyle(() => { - let footerTranslateY = animatedFooterPosition.value; + let footerTranslateY = animatedFooterPosition.get(); /** * Offset the bottom inset only when keyboard is not shown */ - if (animatedKeyboardState.value !== KEYBOARD_STATE.SHOWN) { + if (animatedKeyboardState.get() !== KEYBOARD_STATE.SHOWN) { footerTranslateY = footerTranslateY - bottomInset; } @@ -49,24 +58,54 @@ function BottomSheetFooterComponent({ layout: { height }, }, }: LayoutChangeEvent) => { - animatedFooterHeight.value = height; + animatedFooterHeight.set(height); + + if (__DEV__) { + print({ + component: 'BottomSheetFooter', + method: 'handleContainerLayout', + category: 'layout', + params: { + height, + }, + }); + } + }, + [animatedFooterHeight] + ); + const handleBoundingClientRect = useCallback( + ({ height }: BoundingClientRect) => { + animatedFooterHeight.set(height); + + if (__DEV__) { + print({ + component: 'BottomSheetFooter', + method: 'handleBoundingClientRect', + category: 'layout', + params: { + height, + }, + }); + } }, [animatedFooterHeight] ); //#endregion + //#region effects + useBoundingClientRect(ref, handleBoundingClientRect); + //#endregion + return children !== null ? ( - {typeof children === 'function' ? children() : children} + {children} ) : null; } -const BottomSheetFooter = memo(BottomSheetFooterComponent); +export const BottomSheetFooter = memo(BottomSheetFooterComponent); BottomSheetFooter.displayName = 'BottomSheetFooter'; - -export default BottomSheetFooter; diff --git a/src/components/bottomSheetFooterContainer/BottomSheetFooterContainer.tsx b/src/components/bottomSheetFooter/BottomSheetFooterContainer.tsx similarity index 54% rename from src/components/bottomSheetFooterContainer/BottomSheetFooterContainer.tsx rename to src/components/bottomSheetFooter/BottomSheetFooterContainer.tsx index 0ddd8e01a..953879e86 100644 --- a/src/components/bottomSheetFooterContainer/BottomSheetFooterContainer.tsx +++ b/src/components/bottomSheetFooter/BottomSheetFooterContainer.tsx @@ -1,11 +1,12 @@ -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 { INITIAL_HANDLE_HEIGHT } from '../bottomSheet/constants'; import type { BottomSheetFooterContainerProps } from './types'; const BottomSheetFooterContainerComponent = ({ - footerComponent: FooterComponent, + renderFooter, }: BottomSheetFooterContainerProps) => { //#region hooks const { @@ -20,21 +21,23 @@ const BottomSheetFooterContainerComponent = ({ //#region variables const animatedFooterPosition = useDerivedValue(() => { - const keyboardHeight = animatedKeyboardHeightInContainer.value; - let footerTranslateY = Math.max( - 0, - animatedContainerHeight.value - animatedPosition.value - ); + const handleHeight = animatedHandleHeight.get(); + if (handleHeight === INITIAL_HANDLE_HEIGHT) { + return 0; + } - if (animatedKeyboardState.value === KEYBOARD_STATE.SHOWN) { + const keyboardHeight = animatedKeyboardHeightInContainer.get(); + const containerHeight = animatedContainerHeight.get(); + const position = animatedPosition.get(); + const keyboardState = animatedKeyboardState.get(); + const footerHeight = animatedFooterHeight.get(); + + let footerTranslateY = Math.max(0, containerHeight - position); + if (keyboardState === KEYBOARD_STATE.SHOWN) { footerTranslateY = footerTranslateY - keyboardHeight; } - footerTranslateY = - footerTranslateY - - animatedFooterHeight.value - - animatedHandleHeight.value; - + footerTranslateY = footerTranslateY - footerHeight - handleHeight; return footerTranslateY; }, [ animatedKeyboardHeightInContainer, @@ -46,10 +49,10 @@ const BottomSheetFooterContainerComponent = ({ ]); //#endregion - return ; + return renderFooter({ animatedFooterPosition }); }; -const BottomSheetFooterContainer = memo(BottomSheetFooterContainerComponent); +export const BottomSheetFooterContainer = memo( + BottomSheetFooterContainerComponent +); BottomSheetFooterContainer.displayName = 'BottomSheetFooterContainer'; - -export default BottomSheetFooterContainer; diff --git a/src/components/bottomSheetFooter/index.ts b/src/components/bottomSheetFooter/index.ts index f20231923..73d4c88ce 100644 --- a/src/components/bottomSheetFooter/index.ts +++ b/src/components/bottomSheetFooter/index.ts @@ -1,2 +1,3 @@ -export { default } from './BottomSheetFooter'; +export { BottomSheetFooter } from './BottomSheetFooter'; +export { BottomSheetFooterContainer } from './BottomSheetFooterContainer'; export type { BottomSheetFooterProps } from './types'; 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..298ea59fe 100644 --- a/src/components/bottomSheetFooter/types.d.ts +++ b/src/components/bottomSheetFooter/types.d.ts @@ -1,14 +1,15 @@ 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'; +import type { BottomSheetProps } from '../bottomSheet/types'; export interface BottomSheetFooterProps { /** * Calculated footer animated position. * - * @type Animated.SharedValue + * @type SharedValue */ - animatedFooterPosition: Animated.SharedValue; + animatedFooterPosition: SharedValue; } export interface BottomSheetDefaultFooterProps extends BottomSheetFooterProps { @@ -31,7 +32,10 @@ export interface BottomSheetDefaultFooterProps extends BottomSheetFooterProps { /** * Component to be placed in the footer. * - * @type {ReactNode | ReactNode[]} + * @type {ReactNode|ReactNode[]} */ children?: ReactNode | ReactNode[]; } + +export interface BottomSheetFooterContainerProps + extends Required> {} diff --git a/src/components/bottomSheetFooterContainer/types.d.ts b/src/components/bottomSheetFooterContainer/types.d.ts deleted file mode 100644 index 2ba8cf705..000000000 --- a/src/components/bottomSheetFooterContainer/types.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { BottomSheetProps } from '../bottomSheet'; - -export interface BottomSheetFooterContainerProps - 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/bottomSheetGestureHandlersProvider/types.d.ts b/src/components/bottomSheetGestureHandlersProvider/types.d.ts index 9898f1b8b..08af44756 100644 --- a/src/components/bottomSheetGestureHandlersProvider/types.d.ts +++ b/src/components/bottomSheetGestureHandlersProvider/types.d.ts @@ -1,7 +1,8 @@ import type { ReactChild } from 'react'; +import React from 'react'; import type { BottomSheetProps } from '../bottomSheet/types'; export interface BottomSheetGestureHandlersProviderProps extends Pick { - children: ReactChild | ReactChild[]; + children?: React.ReactNode; } diff --git a/src/components/bottomSheetHandle/BottomSheetHandle.tsx b/src/components/bottomSheetHandle/BottomSheetHandle.tsx index ea9800571..0c95d13a1 100644 --- a/src/components/bottomSheetHandle/BottomSheetHandle.tsx +++ b/src/components/bottomSheetHandle/BottomSheetHandle.tsx @@ -1,34 +1,49 @@ import React, { memo, useMemo } from 'react'; -import Animated from 'react-native-reanimated'; +import { StyleSheet, View } from 'react-native'; +import { + DEFAULT_ACCESSIBILITY_HINT, + DEFAULT_ACCESSIBILITY_LABEL, + DEFAULT_ACCESSIBILITY_ROLE, + DEFAULT_ACCESSIBLE, +} from './constants'; import { styles } from './styles'; import type { BottomSheetDefaultHandleProps } from './types'; -const BottomSheetHandleComponent = ({ +function BottomSheetHandleComponent({ style, indicatorStyle: _indicatorStyle, + accessible = DEFAULT_ACCESSIBLE, + accessibilityRole = DEFAULT_ACCESSIBILITY_ROLE, + accessibilityLabel = DEFAULT_ACCESSIBILITY_LABEL, + accessibilityHint = DEFAULT_ACCESSIBILITY_HINT, children, -}: BottomSheetDefaultHandleProps) => { - // styles +}: BottomSheetDefaultHandleProps) { + //#region styles const containerStyle = useMemo( - () => [styles.container, ...[Array.isArray(style) ? style : [style]]], + () => [styles.container, StyleSheet.flatten(style)], [style] ); const indicatorStyle = useMemo( - () => [ - styles.indicator, - ...[Array.isArray(_indicatorStyle) ? _indicatorStyle : [_indicatorStyle]], - ], + () => [styles.indicator, StyleSheet.flatten(_indicatorStyle)], [_indicatorStyle] ); + //#endregion // render return ( - - + + {children} - + ); -}; +} const BottomSheetHandle = memo(BottomSheetHandleComponent); BottomSheetHandle.displayName = 'BottomSheetHandle'; diff --git a/src/components/bottomSheetHandle/BottomSheetHandleContainer.tsx b/src/components/bottomSheetHandle/BottomSheetHandleContainer.tsx new file mode 100644 index 000000000..9b6a05445 --- /dev/null +++ b/src/components/bottomSheetHandle/BottomSheetHandleContainer.tsx @@ -0,0 +1,180 @@ +import React, { memo, useCallback, useMemo, useRef } from 'react'; +import type { LayoutChangeEvent, View } from 'react-native'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import Animated from 'react-native-reanimated'; +import { + type BoundingClientRect, + useBottomSheetGestureHandlers, + useBottomSheetInternal, + useBoundingClientRect, +} from '../../hooks'; +import { print } from '../../utilities'; +import { DEFAULT_ENABLE_HANDLE_PANNING_GESTURE } from '../bottomSheet/constants'; +import BottomSheetHandle from './BottomSheetHandle'; +import type { BottomSheetHandleContainerProps } from './types'; + +function BottomSheetHandleContainerComponent({ + animatedIndex, + animatedPosition, + simultaneousHandlers: _internalSimultaneousHandlers, + enableHandlePanningGesture = DEFAULT_ENABLE_HANDLE_PANNING_GESTURE, + handleHeight, + handleComponent, + handleStyle: _providedHandleStyle, + handleIndicatorStyle: _providedIndicatorStyle, +}: BottomSheetHandleContainerProps) { + //#region refs + const ref = useRef(null); + //#endregion + + //#region hooks + const { + activeOffsetX, + activeOffsetY, + failOffsetX, + failOffsetY, + waitFor, + simultaneousHandlers: _providedSimultaneousHandlers, + } = useBottomSheetInternal(); + const { handlePanGestureHandler } = useBottomSheetGestureHandlers(); + //#endregion + + //#region variables + const simultaneousHandlers = useMemo(() => { + const refs = []; + + if (_internalSimultaneousHandlers) { + refs.push(_internalSimultaneousHandlers); + } + + if (_providedSimultaneousHandlers) { + if (Array.isArray(_providedSimultaneousHandlers)) { + refs.push(..._providedSimultaneousHandlers); + } else { + refs.push(_providedSimultaneousHandlers); + } + } + + 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 + const handleContainerLayout = useCallback( + function handleContainerLayout({ + nativeEvent: { + layout: { height }, + }, + }: LayoutChangeEvent) { + handleHeight.value = height; + + if (__DEV__) { + print({ + component: BottomSheetHandleContainer.displayName, + method: 'handleContainerLayout', + category: 'layout', + params: { + height, + }, + }); + } + }, + [handleHeight] + ); + const handleBoundingClientRect = useCallback( + ({ height }: BoundingClientRect) => { + handleHeight.value = height; + if (__DEV__) { + print({ + component: BottomSheetHandleContainer.displayName, + method: 'handleBoundingClientRect', + category: 'layout', + params: { + height, + }, + }); + } + }, + [handleHeight] + ); + //#endregion + + //#region effects + useBoundingClientRect(ref, handleBoundingClientRect); + //#endregion + + //#region renders + const HandleComponent = handleComponent ?? BottomSheetHandle; + return ( + + + + + + ); + //#endregion +} + +const BottomSheetHandleContainer = memo(BottomSheetHandleContainerComponent); +BottomSheetHandleContainer.displayName = 'BottomSheetHandleContainer'; + +export default BottomSheetHandleContainer; 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/index.ts b/src/components/bottomSheetHandle/index.ts index 442097b11..931af25d6 100644 --- a/src/components/bottomSheetHandle/index.ts +++ b/src/components/bottomSheetHandle/index.ts @@ -1,2 +1,6 @@ -export { default } from './BottomSheetHandle'; -export type { BottomSheetHandleProps } from './types'; +export { default as BottomSheetHandle } from './BottomSheetHandle'; +export { default as BottomSheetHandleContainer } from './BottomSheetHandleContainer'; +export type { + BottomSheetHandleProps, + BottomSheetHandleContainerProps, +} from './types'; diff --git a/src/components/bottomSheetHandle/styles.ts b/src/components/bottomSheetHandle/styles.ts index c7d8f025c..abf2a7fe8 100644 --- a/src/components/bottomSheetHandle/styles.ts +++ b/src/components/bottomSheetHandle/styles.ts @@ -4,6 +4,8 @@ import { WINDOW_WIDTH } from '../../constants'; export const styles = StyleSheet.create({ container: { padding: 10, + // @ts-ignore supported on web + cursor: 'grab', }, indicator: { diff --git a/src/components/bottomSheetHandle/types.d.ts b/src/components/bottomSheetHandle/types.d.ts index 20cc2bc20..4d6a38fce 100644 --- a/src/components/bottomSheetHandle/types.d.ts +++ b/src/components/bottomSheetHandle/types.d.ts @@ -1,23 +1,30 @@ 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 { View, ViewProps } from 'react-native'; +import type { PanGestureHandlerProperties } from 'react-native-gesture-handler'; +import type { AnimateProps, SharedValue } from 'react-native-reanimated'; +import type { useInteractivePanGestureHandlerConfigs } from '../../hooks/useGestureHandler'; +import type { + BottomSheetVariables, + NullableAccessibilityProps, +} from '../../types'; +import type { BottomSheetProps } from '../bottomSheet'; -export interface BottomSheetHandleProps extends BottomSheetVariables {} - -export interface BottomSheetDefaultHandleProps extends BottomSheetHandleProps { +export type BottomSheetHandleProps = BottomSheetVariables; +export interface BottomSheetDefaultHandleProps + extends BottomSheetHandleProps, + NullableAccessibilityProps { /** * View style to be applied to the handle container. - * @type Animated.AnimateStyle | ViewStyle + * @type ViewStyle * @default undefined */ - style?: AnimateProps['style']; + style?: ViewProps['style']; /** * View style to be applied to the handle indicator. - * @type Animated.AnimateStyle | ViewStyle + * @type ViewStyle * @default undefined */ - indicatorStyle?: AnimateProps['style']; + indicatorStyle?: ViewProps['style']; /** * Content to be added below the indicator. * @type React.ReactNode | React.ReactNode[]; @@ -25,3 +32,25 @@ export interface BottomSheetDefaultHandleProps extends BottomSheetHandleProps { */ children?: React.ReactNode | React.ReactNode[]; } + +export type BottomSheetHandleContainerProps = Pick< + PanGestureHandlerProperties, + 'simultaneousHandlers' +> & + Pick< + BottomSheetProps, + | 'enableHandlePanningGesture' + | 'handleIndicatorStyle' + | 'handleStyle' + | 'handleComponent' + > & + Pick< + useInteractivePanGestureHandlerConfigs, + | 'enableOverDrag' + | 'enablePanDownToClose' + | 'overDragResistanceFactor' + | 'keyboardBehavior' + > & + BottomSheetHandleProps & { + handleHeight: SharedValue; + }; diff --git a/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx b/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx deleted file mode 100644 index 2219e0f1d..000000000 --- a/src/components/bottomSheetHandleContainer/BottomSheetHandleContainer.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React, { memo, useCallback, useMemo } from 'react'; -import type { LayoutChangeEvent } from 'react-native'; -import { PanGestureHandler } 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 type { BottomSheetHandleContainerProps } from './types'; - -function BottomSheetHandleContainerComponent({ - animatedIndex, - animatedPosition, - simultaneousHandlers: _internalSimultaneousHandlers, - enableHandlePanningGesture, - handleHeight, - handleComponent: _providedHandleComponent, - handleStyle: _providedHandleStyle, - handleIndicatorStyle: _providedIndicatorStyle, -}: BottomSheetHandleContainerProps) { - //#region hooks - const { - activeOffsetX, - activeOffsetY, - failOffsetX, - failOffsetY, - waitFor, - simultaneousHandlers: _providedSimultaneousHandlers, - } = useBottomSheetInternal(); - const { handlePanGestureHandler } = useBottomSheetGestureHandlers(); - //#endregion - - //#region variables - const simultaneousHandlers = useMemo(() => { - const refs = []; - - if (_internalSimultaneousHandlers) { - refs.push(_internalSimultaneousHandlers); - } - - if (_providedSimultaneousHandlers) { - if (Array.isArray(_providedSimultaneousHandlers)) { - refs.push(..._providedSimultaneousHandlers); - } else { - refs.push(_providedSimultaneousHandlers); - } - } - - return refs; - }, [_providedSimultaneousHandlers, _internalSimultaneousHandlers]); - //#endregion - - //#region callbacks - const handleContainerLayout = useCallback( - function handleContainerLayout({ - nativeEvent: { - layout: { height }, - }, - }: LayoutChangeEvent) { - handleHeight.value = height; - - print({ - component: BottomSheetHandleContainer.displayName, - method: 'handleContainerLayout', - params: { - height, - }, - }); - }, - [handleHeight] - ); - //#endregion - - //#region renders - const HandleComponent = - _providedHandleComponent === undefined - ? BottomSheetHandle - : _providedHandleComponent; - return HandleComponent !== null ? ( - - - - - - ) : null; - //#endregion -} - -const BottomSheetHandleContainer = memo(BottomSheetHandleContainerComponent); -BottomSheetHandleContainer.displayName = 'BottomSheetHandleContainer'; - -export default BottomSheetHandleContainer; diff --git a/src/components/bottomSheetHandleContainer/index.ts b/src/components/bottomSheetHandleContainer/index.ts deleted file mode 100644 index 959f0ad03..000000000 --- a/src/components/bottomSheetHandleContainer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './BottomSheetHandleContainer'; diff --git a/src/components/bottomSheetHandleContainer/types.d.ts b/src/components/bottomSheetHandleContainer/types.d.ts deleted file mode 100644 index c3c2ae928..000000000 --- a/src/components/bottomSheetHandleContainer/types.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { PanGestureHandlerProperties } from 'react-native-gesture-handler'; -import type Animated from 'react-native-reanimated'; -import type { BottomSheetProps } from '../bottomSheet'; -import type { BottomSheetHandleProps } from '../bottomSheetHandle'; -import type { useInteractivePanGestureHandlerConfigs } from '../../hooks/useGestureHandler'; - -export interface BottomSheetHandleContainerProps - extends Pick, - Pick< - BottomSheetProps, - | 'handleComponent' - | 'enableHandlePanningGesture' - | 'handleIndicatorStyle' - | 'handleStyle' - >, - Pick< - useInteractivePanGestureHandlerConfigs, - | 'enableOverDrag' - | 'enablePanDownToClose' - | 'overDragResistanceFactor' - | 'keyboardBehavior' - >, - BottomSheetHandleProps { - handleHeight: Animated.SharedValue; -} diff --git a/src/components/bottomSheetHostingContainer/BottomSheetHostingContainer.tsx b/src/components/bottomSheetHostingContainer/BottomSheetHostingContainer.tsx new file mode 100644 index 000000000..ae9eea793 --- /dev/null +++ b/src/components/bottomSheetHostingContainer/BottomSheetHostingContainer.tsx @@ -0,0 +1,103 @@ +import React, { memo, useMemo, useRef } from 'react'; +import { + type LayoutChangeEvent, + StatusBar, + type StyleProp, + View, + type ViewStyle, +} from 'react-native'; +import { WINDOW_HEIGHT } from '../../constants'; +import { useStableCallback } from '../../hooks'; +import { print } from '../../utilities'; +import { styles } from './styles'; +import type { BottomSheetHostingContainerProps } from './types'; + +function BottomSheetHostingContainerComponent({ + containerHeight, + containerOffset, + topInset = 0, + bottomInset = 0, + shouldCalculateHeight = true, + detached, + style, + children, +}: BottomSheetHostingContainerProps) { + //#region refs + const containerRef = useRef(null); + //#endregion + + //#region styles + const containerStyle = useMemo>( + () => [ + style, + styles.container, + { + top: topInset, + bottom: bottomInset, + overflow: detached ? 'visible' : 'hidden', + }, + ], + [style, detached, topInset, bottomInset] + ); + //#endregion + + //#region callbacks + const handleLayoutEvent = useStableCallback(function handleLayoutEvent({ + nativeEvent: { + layout: { height }, + }, + }: LayoutChangeEvent) { + containerHeight.value = height; + containerRef.current?.measure((_x, _y, _width, _height, _pageX, pageY) => { + if (!containerOffset.value) { + return; + } + containerOffset.value = { + top: pageY ?? 0, + left: 0, + right: 0, + bottom: Math.max( + 0, + WINDOW_HEIGHT - + ((pageY ?? 0) + height + (StatusBar.currentHeight ?? 0)) + ), + }; + }); + + if (__DEV__) { + print({ + component: 'BottomSheetHostingContainer', + method: 'handleLayoutEvent', + category: 'layout', + params: { + height, + top: containerOffset.value?.top, + left: containerOffset.value?.left, + right: containerOffset.value?.right, + bottom: containerOffset.value?.bottom, + WINDOW_HEIGHT, + }, + }); + } + }); + //#endregion + + //#region render + return ( + + {children} + + ); + //#endregion +} + +export const BottomSheetHostingContainer = memo( + BottomSheetHostingContainerComponent +); +BottomSheetHostingContainer.displayName = 'BottomSheetHostingContainer'; diff --git a/src/components/bottomSheetHostingContainer/index.ts b/src/components/bottomSheetHostingContainer/index.ts new file mode 100644 index 000000000..3aa301856 --- /dev/null +++ b/src/components/bottomSheetHostingContainer/index.ts @@ -0,0 +1,2 @@ +export { BottomSheetHostingContainer } from './BottomSheetHostingContainer'; +export type { BottomSheetHostingContainerProps } from './types'; diff --git a/src/components/bottomSheetContainer/styles.ts b/src/components/bottomSheetHostingContainer/styles.ts similarity index 53% rename from src/components/bottomSheetContainer/styles.ts rename to src/components/bottomSheetHostingContainer/styles.ts index 04f247de5..95826d9cc 100644 --- a/src/components/bottomSheetContainer/styles.ts +++ b/src/components/bottomSheetHostingContainer/styles.ts @@ -1,7 +1,5 @@ import { StyleSheet } from 'react-native'; export const styles = StyleSheet.create({ - container: { - ...StyleSheet.absoluteFillObject, - }, + container: { ...StyleSheet.absoluteFillObject, pointerEvents: 'box-none' }, }); diff --git a/src/components/bottomSheetBackdropContainer/styles.ts b/src/components/bottomSheetHostingContainer/styles.web.ts similarity index 100% rename from src/components/bottomSheetBackdropContainer/styles.ts rename to src/components/bottomSheetHostingContainer/styles.web.ts index 7a3253f94..42836a23f 100644 --- a/src/components/bottomSheetBackdropContainer/styles.ts +++ b/src/components/bottomSheetHostingContainer/styles.web.ts @@ -3,9 +3,9 @@ import { StyleSheet } from 'react-native'; export const styles = StyleSheet.create({ container: { position: 'absolute', - top: 0, left: 0, right: 0, bottom: 0, + top: 0, }, }); diff --git a/src/components/bottomSheetContainer/types.d.ts b/src/components/bottomSheetHostingContainer/types.d.ts similarity index 59% rename from src/components/bottomSheetContainer/types.d.ts rename to src/components/bottomSheetHostingContainer/types.d.ts index a2193eddb..9687eaf46 100644 --- a/src/components/bottomSheetContainer/types.d.ts +++ b/src/components/bottomSheetHostingContainer/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 +export interface BottomSheetHostingContainerProps 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/bottomSheetModal/BottomSheetModal.tsx b/src/components/bottomSheetModal/BottomSheetModal.tsx index e0070c5ce..fddf9f9ed 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,23 +62,26 @@ const BottomSheetModalComponent = forwardRef< } = props; //#region state - const [{ mount, data }, setState] = useState(INITIAL_STATE); + const [{ mount, data }, setState] = + useState>(INITIAL_STATE); //#endregion //#region hooks const { + hostName, containerHeight, containerOffset, mountSheet, unmountSheet, willUnmountSheet, } = useBottomSheetModalInternal(); - const { removePortal: unmountPortal } = usePortal(); + const { removePortal: unmountPortal } = usePortal(hostName); //#endregion //#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 +94,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 +106,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 +156,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 +260,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 +291,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 +312,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 +350,64 @@ 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, + fromPosition: number, + toPosition: number, + source: ANIMATION_SOURCE + ) => { + nextIndexRef.current = toIndex; + + if (_providedOnAnimate) { + _providedOnAnimate(fromIndex, toIndex, fromPosition, toPosition, 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,37 +440,50 @@ 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..3312774f6 100644 --- a/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx +++ b/src/components/bottomSheetModalProvider/BottomSheetModalProvider.tsx @@ -1,17 +1,21 @@ +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 { id } from '../../utilities/id'; import { INITIAL_CONTAINER_HEIGHT, INITIAL_CONTAINER_OFFSET, } from '../bottomSheet/constants'; -import type { BottomSheetModalStackBehavior } from '../bottomSheetModal'; +import { BottomSheetHostingContainer } from '../bottomSheetHostingContainer'; +import type { + BottomSheetModalPrivateMethods, + BottomSheetModalStackBehavior, +} from '../bottomSheetModal'; import type { BottomSheetModalProviderProps, BottomSheetModalRef, @@ -26,12 +30,17 @@ const BottomSheetModalProviderWrapper = ({ //#endregion //#region variables + const hostName = useMemo(() => `bottom-sheet-portal-${id()}`, []); const sheetsQueueRef = useRef([]); //#endregion //#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 +59,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(); + } } /** @@ -162,6 +177,7 @@ const BottomSheetModalProviderWrapper = ({ ); const internalContextVariables = useMemo( () => ({ + hostName, containerHeight: animatedContainerHeight, containerOffset: animatedContainerOffset, mountSheet: handleMountSheet, @@ -169,6 +185,7 @@ const BottomSheetModalProviderWrapper = ({ willUnmountSheet: handleWillUnmountSheet, }), [ + hostName, animatedContainerHeight, animatedContainerOffset, handleMountSheet, @@ -182,12 +199,11 @@ const BottomSheetModalProviderWrapper = ({ return ( - - {children} + {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..86a1e1880 --- /dev/null +++ b/src/components/bottomSheetScrollable/BottomSheetFlashList.tsx @@ -0,0 +1,83 @@ +// @ts-ignore +import type { FlashListProps } from '@shopify/flash-list'; +import React, { forwardRef, memo, type Ref, useMemo } from 'react'; +import type { ScrollViewProps } from 'react-native'; +import BottomSheetScrollView from './BottomSheetScrollView'; +import type { + BottomSheetScrollViewMethods, + BottomSheetScrollableProps, +} 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 (_) {} + +export type BottomSheetFlashListProps = Omit< + FlashListProps, + 'decelerationRate' | 'scrollEventThrottle' +> & + BottomSheetScrollableProps & { + ref?: Ref; + }; + +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 BottomSheetFlashList = memo(BottomSheetFlashListComponent); + +export default BottomSheetFlashList as ( + props: BottomSheetFlashListProps +) => ReturnType; diff --git a/src/components/bottomSheetScrollable/BottomSheetFlashList.web.tsx b/src/components/bottomSheetScrollable/BottomSheetFlashList.web.tsx new file mode 100644 index 000000000..461f67a0a --- /dev/null +++ b/src/components/bottomSheetScrollable/BottomSheetFlashList.web.tsx @@ -0,0 +1 @@ +export default () => null; 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..8fff6762d --- /dev/null +++ b/src/components/bottomSheetScrollable/ScrollableContainer.web.tsx @@ -0,0 +1,102 @@ +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 { useBottomSheetInternal } from '../../hooks'; +import { INITIAL_CONTAINER_HEIGHT } from '../bottomSheet/constants'; +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 hooks + const { animatedContentHeight } = useBottomSheetInternal(); + //#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; + } + + /** + * early exit if the content height been calculated. + */ + if (animatedContentHeight.get() !== INITIAL_CONTAINER_HEIGHT) { + return; + } + // @ts-ignore + window.requestAnimationFrame(() => { + // @ts-ignore + setContentSize(event.nativeEvent.target.clientHeight); + }); + } + }, + [onLayout, setContentSize, animatedContentHeight] + ); + //#endregion + return ( + + + + ); +}); diff --git a/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx b/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx index 49237cb9d..6101812f8 100644 --- a/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx +++ b/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx @@ -1,28 +1,34 @@ -import React, { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'; -import { Platform } from 'react-native'; -import { useAnimatedProps, useAnimatedStyle } from 'react-native-reanimated'; -import { NativeViewGestureHandler } from 'react-native-gesture-handler'; -import BottomSheetDraggableView from '../bottomSheetDraggableView'; -import BottomSheetRefreshControl from '../bottomSheetRefreshControl'; +import React, { + forwardRef, + useContext, + useImperativeHandle, + useMemo, +} from 'react'; +import { Gesture } from 'react-native-gesture-handler'; +import { useAnimatedProps } from 'react-native-reanimated'; 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 { + useBottomSheetContentContainerStyle, + 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, @@ -32,68 +38,81 @@ export function createBottomSheetScrollableComponent( overScrollMode = 'never', keyboardDismissMode = 'interactive', showsVerticalScrollIndicator = true, - style, + contentContainerStyle: _providedContentContainerStyle, refreshing, onRefresh, progressViewOffset, refreshControl, + preserveScrollMomentum, // events onScroll, onScrollBeginDrag, onScrollEndDrag, + lockableScrollableContentOffsetY, + 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, + lockableScrollableContentOffsetY ); - const { - enableContentPanningGesture, - animatedFooterHeight, - animatedScrollableState, - } = useBottomSheetInternal(); + const { 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, preserveScrollMomentum] + ); + + 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 //#region styles - const containerAnimatedStyle = useAnimatedStyle( - () => ({ - marginBottom: enableFooterMarginAdjustment - ? animatedFooterHeight.value - : 0, - }), - [enableFooterMarginAdjustment] + const contentContainerStyle = useBottomSheetContentContainerStyle( + enableFooterMarginAdjustment, + _providedContentContainerStyle ); - const containerStyle = useMemo(() => { - return enableFooterMarginAdjustment - ? [ - ...(style ? ('length' in style ? style : [style]) : []), - containerAnimatedStyle, - ] - : style; - }, [enableFooterMarginAdjustment, style, containerAnimatedStyle]); //#endregion //#region effects @@ -109,75 +128,25 @@ export function createBottomSheetScrollableComponent( //#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..bbd3fb3b8 100644 --- a/src/components/bottomSheetScrollable/types.d.ts +++ b/src/components/bottomSheetScrollable/types.d.ts @@ -6,15 +6,15 @@ 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'; @@ -47,6 +47,16 @@ export interface BottomSheetScrollableProps { * @default useScrollEventsHandlersDefault */ scrollEventsHandlersHook?: ScrollEventsHandlersHookType; + + /** + * Whether or not to preserve scroll momentum when expanding a scrollable bottom sheet component.Add commentMore actions + */ + preserveScrollMomentum?: boolean; + + /** + * The optional lockable scrollable content offset ref, which will remain the same value when scrollable is locked.Add commentMore actions + */ + lockableScrollableContentOffsetY?: Animated.SharedValue; } export type ScrollableProps = @@ -57,7 +67,7 @@ export type ScrollableProps = //#region FlatList export type BottomSheetFlatListProps = Omit< Animated.AnimateProps>, - 'decelerationRate' | 'onScroll' | 'scrollEventThrottle' + 'decelerationRate' | 'scrollEventThrottle' > & BottomSheetScrollableProps & { ref?: Ref; @@ -87,6 +97,7 @@ export interface BottomSheetFlatListMethods { */ scrollToItem: (params: { animated?: boolean | null; + // biome-ignore lint: to be addressed! item: any; viewPosition?: number; }) => void; @@ -114,7 +125,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 @@ -125,9 +136,10 @@ export interface BottomSheetFlatListMethods { | null | undefined; + // biome-ignore lint: to be addressed! getScrollableNode: () => any; - // TODO: use `unknown` instead of `any` for Typescript >= 3.0 + // biome-ignore lint: to be addressed! setNativeProps: (props: { [key: string]: any }) => void; } //#endregion @@ -175,11 +187,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 +211,7 @@ export interface BottomSheetScrollViewMethods { //#endregion //#region SectionList -type BottomSheetSectionListProps = Omit< +export type BottomSheetSectionListProps = Omit< Animated.AnimateProps>, 'decelerationRate' | 'scrollEventThrottle' > & @@ -231,7 +244,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 +272,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..fa46e9f15 --- /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.set(contentHeight); + }, + [enableDynamicSizing, animatedContentHeight] + ); + //#endregion + + return { + setContentSize, + }; +} diff --git a/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx b/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx index eae09f7d4..2a74c6dd5 100644 --- a/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx +++ b/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx @@ -1,4 +1,9 @@ -import React, { memo, useCallback, forwardRef } from 'react'; +import React, { memo, useCallback, forwardRef, useEffect } from 'react'; +import type { + FocusEvent, + BlurEvent, + TextInputFocusEventData, +} from 'react-native'; import { TextInput } from 'react-native-gesture-handler'; import { useBottomSheetInternal } from '../../hooks'; import type { BottomSheetTextInputProps } from './types'; @@ -13,7 +18,7 @@ const BottomSheetTextInputComponent = forwardRef< //#region callbacks const handleOnFocus = useCallback( - args => { + (args: FocusEvent) => { shouldHandleKeyboardEvents.value = true; if (onFocus) { onFocus(args); @@ -22,7 +27,7 @@ const BottomSheetTextInputComponent = forwardRef< [onFocus, shouldHandleKeyboardEvents] ); const handleOnBlur = useCallback( - args => { + (args: BlurEvent) => { shouldHandleKeyboardEvents.value = false; if (onBlur) { onBlur(args); @@ -32,6 +37,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] + //#region styles + const containerStyle = useBottomSheetContentContainerStyle( + enableFooterMarginAdjustment, + _providedStyle ); - const containerStyle = useMemo( - () => [style, containerAnimatedStyle], - [style, containerAnimatedStyle] + const style = useMemo( + () => [containerStyle, styles.container], + [containerStyle] ); + //#endregion - // callback + //#region callbacks const handleSettingScrollable = useCallback(() => { animatedScrollableContentOffsetY.value = 0; animatedScrollableType.value = SCROLLABLE_TYPE.VIEW; }, [animatedScrollableContentOffsetY, animatedScrollableType]); + const handleLayout = useCallback( + (event: LayoutChangeEvent) => { + if (enableDynamicSizing) { + animatedContentHeight.set(event.nativeEvent.layout.height); + } - // effects + if (onLayout) { + onLayout(event); + } + + if (__DEV__) { + print({ + component: BottomSheetView.displayName, + method: 'handleLayout', + category: 'layout', + params: { + height: event.nativeEvent.layout.height, + }, + }); + } + }, + [onLayout, animatedContentHeight, enableDynamicSizing] + ); + //#endregion + + //#region effects useFocusHook(handleSettingScrollable); + //#endregion //render return ( - + {children} - + ); } diff --git a/src/components/bottomSheetView/styles.ts b/src/components/bottomSheetView/styles.ts index 0216192e5..7ad63813a 100644 --- a/src/components/bottomSheetView/styles.ts +++ b/src/components/bottomSheetView/styles.ts @@ -2,6 +2,6 @@ import { StyleSheet } from 'react-native'; export const styles = StyleSheet.create({ container: { - // flex: 1, + // flex: 1 }, }); 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..14e5f1568 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,57 +16,52 @@ 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' + | 'enableBlurKeyboardOnGesture' | '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; + animatedSheetHeight: SharedValue; + animatedHighestSnapPoint: SharedValue; + animatedClosedPosition: SharedValue; + animatedFooterHeight: SharedValue; + animatedHandleHeight: SharedValue; + animatedKeyboardHeight: SharedValue; + animatedKeyboardHeightInContainer: SharedValue; + animatedScrollableType: SharedValue; + animatedScrollableContentOffsetY: SharedValue; + animatedScrollableOverrideState: SharedValue; + isScrollableRefreshable: SharedValue; + isContentHeightFixed: SharedValue; + isInTemporaryPosition: SharedValue; + shouldHandleKeyboardEvents: SharedValue; // methods stopAnimation: () => void; animateToPosition: AnimateToPositionType; setScrollableRef: (ref: ScrollableRef) => void; - removeScrollableRef: (ref: RefObject) => void; + removeScrollableRef: (ref: RefObject) => void; } export const BottomSheetInternalContext = diff --git a/src/contexts/modal/internal.ts b/src/contexts/modal/internal.ts index c6f73d605..f26de3139 100644 --- a/src/contexts/modal/internal.ts +++ b/src/contexts/modal/internal.ts @@ -1,15 +1,18 @@ -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>; + hostName: string; + 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..5f9704bab 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -13,12 +13,16 @@ export { useScrollHandler } from './useScrollHandler'; // gestures export { useGestureHandler } from './useGestureHandler'; export { useGestureEventsHandlersDefault } from './useGestureEventsHandlersDefault'; +export { useBottomSheetGestureHandlers } from './useBottomSheetGestureHandlers'; // utilities 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'; +export { + useBoundingClientRect, + type BoundingClientRect, +} from './useBoundingClientRect'; +export { useBottomSheetContentContainerStyle } from './useBottomSheetContentContainerStyle'; diff --git a/src/hooks/useAnimatedSnapPoints.ts b/src/hooks/useAnimatedSnapPoints.ts new file mode 100644 index 000000000..8a4e6a422 --- /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, + 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/useBottomSheetContentContainerStyle.ts b/src/hooks/useBottomSheetContentContainerStyle.ts new file mode 100644 index 000000000..88b5638cb --- /dev/null +++ b/src/hooks/useBottomSheetContentContainerStyle.ts @@ -0,0 +1,86 @@ +import { useMemo, useState } from 'react'; +import { + Platform, + StyleSheet, + type ViewProps, + type ViewStyle, +} from 'react-native'; +import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; +import { useBottomSheetInternal } from './useBottomSheetInternal'; + +export function useBottomSheetContentContainerStyle( + enableFooterMarginAdjustment: boolean, + _style?: ViewProps['style'] +) { + const [footerHeight, setFooterHeight] = useState(0); + //#region hooks + const { animatedFooterHeight, animatedContentHeight } = + useBottomSheetInternal(); + //#endregion + + //#region styles + const flattenStyle = useMemo(() => { + return !_style + ? {} + : Array.isArray(_style) + ? // @ts-ignore + (StyleSheet.compose(..._style) as ViewStyle) + : (_style as ViewStyle); + }, [_style]); + const style = useMemo(() => { + if (!enableFooterMarginAdjustment) { + return flattenStyle; + } + + let currentBottomPadding = 0; + if (flattenStyle && typeof flattenStyle === 'object') { + const { paddingBottom, padding, paddingVertical } = flattenStyle; + if (paddingBottom !== undefined && typeof paddingBottom === 'number') { + currentBottomPadding = paddingBottom; + } else if ( + paddingVertical !== undefined && + typeof paddingVertical === 'number' + ) { + currentBottomPadding = paddingVertical; + } else if (padding !== undefined && typeof padding === 'number') { + currentBottomPadding = padding; + } + } + + return [ + flattenStyle, + { + paddingBottom: currentBottomPadding + footerHeight, + overflow: 'visible', + }, + ]; + }, [footerHeight, enableFooterMarginAdjustment, flattenStyle]); + //#endregion + + //#region effects + useAnimatedReaction( + () => animatedFooterHeight.get(), + (result, previousFooterHeight) => { + if (!enableFooterMarginAdjustment) { + return; + } + runOnJS(setFooterHeight)(result); + + if (Platform.OS === 'web') { + /** + * a reaction that will append the footer height to the content + * height if margin adjustment is true. + * + * This is needed due to the web layout the footer after the content. + */ + if (result && !previousFooterHeight) { + const contentHeight = animatedContentHeight.get(); + animatedContentHeight.set(contentHeight + result); + } + } + }, + [animatedFooterHeight, animatedContentHeight, enableFooterMarginAdjustment] + ); + //#endregion + return style; +} 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/useBoundingClientRect.ts b/src/hooks/useBoundingClientRect.ts new file mode 100644 index 000000000..cc85c8ced --- /dev/null +++ b/src/hooks/useBoundingClientRect.ts @@ -0,0 +1,73 @@ +import { type RefObject, useLayoutEffect } from 'react'; +import type { View } from 'react-native'; +import { isFabricInstalled } from '../utilities/isFabricInstalled'; + +export type BoundingClientRect = { + x: number; + y: number; + width: number; + height: number; + left: number; + right: number; + top: number; + bottom: number; +}; + +/** + * A custom hook that retrieves the bounding client rectangle of a given `ref` element + * and invokes a handler function with the layout information. + * + * This hook is designed to work with React Native's Fabric architecture and provides + * support for both `unstable_getBoundingClientRect` and `getBoundingClientRect` methods. + * + * @param ref - A `RefObject` pointing to a `View` or `null`. The bounding client rectangle + * will be retrieved from this reference. + * @param handler - A callback function that will be invoked with the layout information + * of the referenced element. + * + * @remarks + * - The hook uses `useLayoutEffect` to ensure the layout information is retrieved + * after the DOM updates. + * - The `isFabricInstalled` function is used to determine if the Fabric architecture + * is available. + * - The `unstable_getBoundingClientRect` method is used if available, falling back + * to `getBoundingClientRect` otherwise. + * + * @example + * ```tsx + * const ref = useRef(null); + * useBoundingClientRect(ref, (layout) => { + * console.log('Bounding client rect:', layout); + * }); + * ``` + */ +export function useBoundingClientRect( + ref: RefObject, + handler: (layout: BoundingClientRect) => void +) { + if (!isFabricInstalled()) { + return; + } + + // biome-ignore lint/correctness/useHookAtTopLevel: `isFabricInstalled` is a constant that will not change during the runtime + useLayoutEffect(() => { + if (!ref || !ref.current) { + return; + } + + // @ts-ignore 👉 https://github.com/facebook/react/commit/53b1f69ba + if (ref.current.unstable_getBoundingClientRect !== null) { + // @ts-ignore https://github.com/facebook/react/commit/53b1f69ba + const layout = ref.current.unstable_getBoundingClientRect(); + handler(layout); + return; + } + + // @ts-ignore once it `unstable_getBoundingClientRect` gets stable 🤞. + if (ref.current.getBoundingClientRect !== null) { + // @ts-ignore once it `unstable_getBoundingClientRect` gets stable. + const layout = ref.current.getBoundingClientRect(); + handler(layout); + } + }); +} diff --git a/src/hooks/useGestureEventsHandlersDefault.tsx b/src/hooks/useGestureEventsHandlersDefault.tsx index b44bfa510..20822a85a 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,361 @@ export const useGestureEventsHandlersDefault: GestureEventsHandlersHookType = overDragResistanceFactor, isInTemporaryPosition, isScrollableRefreshable, + enableBlurKeyboardOnGesture, 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; - - /** - * 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; + let initialKeyboardState = animatedKeyboardState.value; + // blur the keyboard when user start dragging the bottom sheet + if ( + enableBlurKeyboardOnGesture && + initialKeyboardState === KEYBOARD_STATE.SHOWN + ) { + initialKeyboardState = KEYBOARD_STATE.HIDDEN; + runOnJS(dismissKeyboard)(); + } - /** - * 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; - } + // store current animated position + context.value = { + ...context.value, + initialPosition: animatedPosition.value, + initialKeyboardState: animatedKeyboardState.value, + }; - /** - * 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 the scrollable content is scrolled, then + * we lock the position. + */ + if (animatedScrollableContentOffsetY.value > 0) { + context.value = { + ...context.value, + isScrollablePositionLocked: true, + }; + } + }, + [ + stopAnimation, + enableBlurKeyboardOnGesture, + animatedPosition, + animatedKeyboardState, + animatedScrollableContentOffsetY, + ] + ); + const handleOnChange: GestureEventHandlerCallbackType = useWorkletCallback( + function handleOnChange(source, { translationY }) { + let highestSnapPoint = animatedHighestSnapPoint.value; - const lowestSnapPoint = enablePanDownToClose - ? animatedContainerHeight.value - : animatedSnapPoints.value[0]; + /** + * 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 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 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; + } - /** - * 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; + const lowestSnapPoint = enablePanDownToClose + ? animatedContainerHeight.value + : animatedSnapPoints.value[0]; - /** - * an accumulated value of starting position with gesture translation y. - */ - const draggedPosition = context.initialPosition + translationY; + /** + * 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; + } - /** - * 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; + /** + * 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; - /** - * 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 starting position with gesture translation y. + */ + const draggedPosition = context.value.initialPosition + translationY; - /** - * 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; - } + /** + * 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; - /** - * 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; + const isSheetAtHighestSnapPoint = + animatedPosition.value === highestSnapPoint; - /** - * 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); + /** + * 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.value.initialPosition >= animatedPosition.value + ) { + if (context.value.initialPosition > animatedPosition.value) { + animateToPosition( + context.value.initialPosition, + ANIMATION_SOURCE.GESTURE, + velocityY / 2 + ); } + return; + } - /** - * calculate the destination point, using redash. - */ - const destinationPoint = snapPoint( - translationY + context.initialPosition, - velocityY, - snapPoints - ); + /** + * 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.value.initialKeyboardState === KEYBOARD_STATE.SHOWN && + animatedPosition.value > context.value.initialPosition + ) { /** - * if destination point is the same as the current position, - * then no need to perform animation. + * 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 (destinationPoint === animatedPosition.value) { - return; + if ( + !( + Platform.OS === 'ios' && + isScrollable && + absoluteY > WINDOW_HEIGHT - animatedKeyboardHeight.value + ) + ) { + runOnJS(dismissKeyboard)(); } + } - const wasGestureHandledByScrollView = - source === GESTURE_SOURCE.SCROLLABLE && - animatedScrollableContentOffsetY.value > 0; - /** - * prevents snapping from top to middle / bottom with repeated interrupted scrolls - */ - if (wasGestureHandledByScrollView && isSheetAtHighestSnapPoint) { - return; - } + /** + * 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, + 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..95f8756fd 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 { @@ -12,7 +12,7 @@ import { useSharedValue, useWorkletCallback, } from 'react-native-reanimated'; -import { KEYBOARD_STATE } from '../constants'; +import { KEYBOARD_STATE, SCREEN_HEIGHT } from '../constants'; const KEYBOARD_EVENT_MAPPER = { KEYBOARD_SHOW: Platform.select({ @@ -27,7 +27,17 @@ const KEYBOARD_EVENT_MAPPER = { }) as KeyboardEventName, }; -export const useKeyboard = () => { +export type UseKeyboardArgs = { + /** + * Determines the bottom offset of the keyboard (e.g. nav bar) and includes it in the keyboard height. + * @default false + */ + includeBottomOffset?: boolean; +} + +export const useKeyboard = ({ + includeBottomOffset +}: UseKeyboardArgs) => { //#region variables const shouldHandleKeyboardEvents = useSharedValue(false); const keyboardState = useSharedValue( @@ -37,12 +47,19 @@ export const useKeyboard = () => { 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, + bottomOffset?: number + ) => { if (state === KEYBOARD_STATE.SHOWN && !shouldHandleKeyboardEvents.value) { /** * if the keyboard event was fired before the `onFocus` on TextInput, @@ -53,11 +70,16 @@ export const useKeyboard = () => { return; } keyboardHeight.value = - state === KEYBOARD_STATE.SHOWN - ? height - : height === 0 - ? keyboardHeight.value - : height; + state === KEYBOARD_STATE.SHOWN ? height : keyboardHeight.value; + + /** + * if keyboard had an bottom offset -android bottom bar-, then + * we add that offset to the keyboard height. + */ + if (bottomOffset && includeBottomOffset) { + keyboardHeight.value = keyboardHeight.value + bottomOffset; + } + keyboardAnimationDuration.value = duration; keyboardAnimationEasing.value = easing; keyboardState.value = state; @@ -74,7 +96,10 @@ export const useKeyboard = () => { KEYBOARD_STATE.SHOWN, event.endCoordinates.height, event.duration, - event.easing + event.easing, + SCREEN_HEIGHT - + event.endCoordinates.height - + event.endCoordinates.screenY ); }; const handleOnKeyboardHide = (event: KeyboardEvent) => { @@ -114,7 +139,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..1fe5844d3 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 + ? 'get' in snapPoints + ? snapPoints.get() + : 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..c95c7f85f 100644 --- a/src/hooks/useScrollEventsHandlersDefault.ts +++ b/src/hooks/useScrollEventsHandlersDefault.ts @@ -1,10 +1,11 @@ -import { scrollTo, useWorkletCallback } from 'react-native-reanimated'; -import { useBottomSheetInternal } from './useBottomSheetInternal'; +import { State } from 'react-native-gesture-handler'; +import { scrollTo, useWorkletCallback, useSharedValue, useAnimatedReaction } from 'react-native-reanimated'; import { ANIMATION_STATE, SCROLLABLE_STATE, SHEET_STATE } from '../constants'; import type { - ScrollEventsHandlersHookType, ScrollEventHandlerCallbackType, + ScrollEventsHandlersHookType, } from '../types'; +import { useBottomSheetInternal } from './useBottomSheetInternal'; export type ScrollEventContextType = { initialContentOffsetY: number; @@ -13,20 +14,33 @@ export type ScrollEventContextType = { export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( scrollableRef, - scrollableContentOffsetY + scrollableContentOffsetY, + lockableScrollableContentOffsetY ) => { // hooks const { animatedSheetState, animatedScrollableState, animatedAnimationState, + animatedHandleGestureState, animatedScrollableContentOffsetY: rootScrollableContentOffsetY, } = useBottomSheetInternal(); + const _lockableScrollableContentOffsetY = useSharedValue(0); + + useAnimatedReaction( + () => _lockableScrollableContentOffsetY.value, + _lockableScrollableContentOffsetY => { + if (lockableScrollableContentOffsetY) { + lockableScrollableContentOffsetY.value = _lockableScrollableContentOffsetY; + } + } + ); + //#region callbacks const handleOnScroll: ScrollEventHandlerCallbackType = useWorkletCallback( - (_, context) => { + ({ contentOffset: { y } }, context) => { /** * if sheet position is extended or fill parent, then we reset * `shouldLockInitialPosition` value to false. @@ -38,15 +52,26 @@ 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 + ? (context.initialContentOffsetY ?? 0) : 0; // @ts-ignore scrollTo(scrollableRef, 0, lockPosition, false); scrollableContentOffsetY.value = lockPosition; + _lockableScrollableContentOffsetY.value = lockPosition; return; } + _lockableScrollableContentOffsetY.value = y; }, [ scrollableRef, @@ -59,6 +84,7 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( useWorkletCallback( ({ contentOffset: { y } }, context) => { scrollableContentOffsetY.value = y; + _lockableScrollableContentOffsetY.value = y; rootScrollableContentOffsetY.value = y; context.initialContentOffsetY = y; @@ -87,15 +113,18 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( ({ contentOffset: { y } }, context) => { 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; } }, @@ -112,15 +141,18 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( ({ contentOffset: { y } }, context) => { 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 = 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..b5962e1a4 100644 --- a/src/hooks/useScrollHandler.ts +++ b/src/hooks/useScrollHandler.ts @@ -1,18 +1,20 @@ import { runOnJS, + SharedValue, 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, + lockableScrollableContentOffsetY?: SharedValue, ) => { // refs const scrollableRef = useAnimatedRef(); @@ -27,7 +29,7 @@ export const useScrollHandler = ( handleOnEndDrag = noop, handleOnMomentumEnd = noop, handleOnMomentumBegin = noop, - } = useScrollEventsHandlers(scrollableRef, scrollableContentOffsetY); + } = useScrollEventsHandlers(scrollableRef, scrollableContentOffsetY, 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..140239c8c --- /dev/null +++ b/src/hooks/useScrollHandler.web.ts @@ -0,0 +1,174 @@ +import { type TouchEvent, useEffect, useRef } from 'react'; +import { useSharedValue } from 'react-native-reanimated'; +import { ANIMATION_STATE, SCROLLABLE_STATE } from '../constants'; +import type { Scrollable, ScrollableEvent } from '../types'; +import { findNodeHandle } from '../utilities/findNodeHandle.web'; +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 && event.cancelable) { + return event.preventDefault(); + } + + if (maybePrevent) { + maybePrevent = false; + + const touchY = event.touches[0].clientY; + const touchYDelta = touchY - lastTouchY; + + if (touchYDelta > 0 && event.cancelable) { + 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.cancelable) { + 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..ed8b88b49 100644 --- a/src/hooks/useScrollable.ts +++ b/src/hooks/useScrollable.ts @@ -1,8 +1,9 @@ -import { useCallback, RefObject, useRef } from 'react'; +import { type RefObject, useCallback, useRef } from 'react'; +import type { NodeHandle } 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'; +import { findNodeHandle } from '../utilities'; export const useScrollable = () => { // refs @@ -22,7 +23,7 @@ export const useScrollable = () => { // 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) { @@ -34,17 +35,17 @@ export const useScrollable = () => { } }, []); - const removeScrollableRef = useCallback((ref: RefObject) => { + 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 diff --git a/src/hooks/useScrollableSetter.ts b/src/hooks/useScrollableSetter.ts index ea7d3c24e..ab7853305 100644 --- a/src/hooks/useScrollableSetter.ts +++ b/src/hooks/useScrollableSetter.ts @@ -1,14 +1,15 @@ -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 type { SharedValue } from 'react-native-reanimated'; +import type { SCROLLABLE_TYPE } from '../constants'; import type { Scrollable } from '../types'; +import { findNodeHandle } from '../utilities'; +import { useBottomSheetInternal } from './useBottomSheetInternal'; export const useScrollableSetter = ( - ref: React.RefObject, + ref: React.RefObject, type: SCROLLABLE_TYPE, - contentOffsetY: Animated.SharedValue, + contentOffsetY: SharedValue, refreshable: boolean, useFocusHook = useEffect ) => { @@ -31,7 +32,7 @@ export const useScrollableSetter = ( isContentHeightFixed.value = false; // set current scrollable ref - const id = getRefNativeTag(ref); + const id = findNodeHandle(ref.current); if (id) { setScrollableRef({ id: id, diff --git a/src/hooks/useStableCallback.ts b/src/hooks/useStableCallback.ts index a868620d5..9c6e172f6 100644 --- a/src/hooks/useStableCallback.ts +++ b/src/hooks/useStableCallback.ts @@ -1,19 +1,26 @@ -import { useRef, useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'; + +type Callback = (...args: T) => R; -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..fe551cbc2 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,14 +25,17 @@ export { BottomSheetSectionList, BottomSheetFlatList, BottomSheetVirtualizedList, + BottomSheetFlashList, } from './components/bottomSheetScrollable'; -export { default as BottomSheetHandle } from './components/bottomSheetHandle'; +export { BottomSheetHandle } from './components/bottomSheetHandle'; export { default as BottomSheetDraggableView } from './components/bottomSheetDraggableView'; export { default as BottomSheetView } from './components/bottomSheetView'; export { default as BottomSheetTextInput } from './components/bottomSheetTextInput'; -export { default as BottomSheetBackdrop } from './components/bottomSheetBackdrop'; -export { default as BottomSheetFooter } from './components/bottomSheetFooter'; -export { default as BottomSheetFooterContainer } from './components/bottomSheetFooterContainer/BottomSheetFooterContainer'; +export { BottomSheetBackdrop } from './components/bottomSheetBackdrop'; +export { + BottomSheetFooter, + BottomSheetFooterContainer, +} from './components/bottomSheetFooter'; // touchables import BottomSheetTouchable from './components/touchables'; @@ -72,5 +74,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..3598533d6 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,14 +1,19 @@ 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, @@ -76,12 +81,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. @@ -110,7 +117,7 @@ export interface BottomSheetVariables { export type Scrollable = FlatList | ScrollView | SectionList; export type ScrollableRef = { id: number; - node: React.RefObject; + node: React.RefObject; }; export type ScrollableEvent = ( event: Pick, 'nativeEvent'> @@ -119,12 +126,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 +136,52 @@ 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 + ref: React.RefObject, + contentOffsetY: SharedValue, + lockableScrollableContentOffsetY?: SharedValue ) => { handleOnScroll?: ScrollEventHandlerCallbackType; handleOnBeginDrag?: ScrollEventHandlerCallbackType; @@ -163,3 +190,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/findNodeHandle.ts b/src/utilities/findNodeHandle.ts new file mode 100644 index 000000000..ad39910f3 --- /dev/null +++ b/src/utilities/findNodeHandle.ts @@ -0,0 +1 @@ +export { findNodeHandle } from 'react-native'; diff --git a/src/utilities/findNodeHandle.web.ts b/src/utilities/findNodeHandle.web.ts new file mode 100644 index 000000000..8e8d5b121 --- /dev/null +++ b/src/utilities/findNodeHandle.web.ts @@ -0,0 +1,33 @@ +import { + type NodeHandle, + findNodeHandle as _findNodeHandle, +} from 'react-native'; + +export function findNodeHandle( + componentOrHandle: Parameters['0'] +) { + let nodeHandle: NodeHandle | null; + try { + nodeHandle = _findNodeHandle(componentOrHandle); + if (nodeHandle) { + return nodeHandle; + } + } catch {} + + try { + // @ts-ignore + nodeHandle = componentOrHandle.getNativeScrollRef(); + if (nodeHandle) { + return nodeHandle; + } + } catch {} + + // @ts-ignore https://github.com/facebook/react-native/blob/a314e34d6ee875830d36e4df1789a897c7262056/packages/virtualized-lists/Lists/VirtualizedList.js#L1252 + nodeHandle = componentOrHandle._scrollRef; + if (nodeHandle) { + return nodeHandle; + } + + console.warn('could not find scrollable ref!'); + return componentOrHandle; +} 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/index.ts b/src/utilities/index.ts index a50b4da63..1dc0c0506 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -3,3 +3,5 @@ export { animate } from './animate'; export { getKeyboardAnimationConfigs } from './getKeyboardAnimationConfigs'; export { print } from './logger'; export { noop, workletNoop } from './noop'; +export { isFabricInstalled } from './isFabricInstalled'; +export { findNodeHandle } from './findNodeHandle'; diff --git a/src/utilities/isFabricInstalled.ts b/src/utilities/isFabricInstalled.ts new file mode 100644 index 000000000..aaacccbad --- /dev/null +++ b/src/utilities/isFabricInstalled.ts @@ -0,0 +1,9 @@ +/** + * Checks if the Fabric renderer is installed in the current environment. + * + * @returns {boolean} `true` if Fabric is installed, otherwise `false`. + */ +export function isFabricInstalled() { + // @ts-ignore + return global?.nativeFabricUIManager != 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/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 + +