diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml
new file mode 100644
index 0000000..57e9454
--- /dev/null
+++ b/.github/workflows/beta-release.yml
@@ -0,0 +1,100 @@
+name: Beta Release
+on:
+ push:
+ branches:
+ - beta
+ pull_request:
+ branches:
+ - beta
+ merge_group:
+ types:
+ - checks_requested
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup
+ uses: ./.github/actions/setup
+
+ - name: Lint files
+ run: yarn lint
+
+ - name: Typecheck files
+ run: yarn typecheck
+
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup
+ uses: ./.github/actions/setup
+
+ - name: Run unit tests
+ run: yarn test --maxWorkers=2 --coverage
+
+ build-library:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup
+ uses: ./.github/actions/setup
+
+ - name: Build package
+ run: yarn prepack
+
+ build-web:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup
+ uses: ./.github/actions/setup
+
+ - name: Build example for Web
+ run: |
+ yarn example expo export:web
+
+ publish-beta:
+ needs: [lint, test, build-library, build-web]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # To publish a GitHub release
+ issues: write # To comment on released issues
+ pull-requests: write # To comment on released pull requests
+ id-token: write # To enable use of OIDC for npm provenance
+ if: github.ref == 'refs/heads/beta'
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Ensures all tags are fetched
+
+ - name: Setup
+ uses: ./.github/actions/setup
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "lts/*" # Use the latest LTS version of Node.js
+ registry-url: 'https://registry.npmjs.org/' # Specify npm registry
+
+ - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
+ run: npm audit signatures # Check the signatures to verify integrity
+
+ - name: Release Beta
+ run: npx semantic-release # Run semantic-release to manage versioning and publishing
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GitHub token for authentication
+
+ # Why NODE_AUTH_TOKEN instead of NPM_TOKEN: https://github.com/semantic-release/semantic-release/issues/2313
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # npm token for publishing package
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9387796..1be8721 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,37 @@
+# [1.4.0-beta.3](https://github.com/JairajJangle/react-native-visibility-sensor/compare/v1.4.0-beta.2...v1.4.0-beta.3) (2025-08-26)
+
+
+### Bug Fixes
+
+* **perf:** reduced unnecessary onPercentChange callback calls when last percent is already 0 ([39e8aa8](https://github.com/JairajJangle/react-native-visibility-sensor/commit/39e8aa847b42eabe9341e4bec96c35f1b8c67972))
+
+# [1.4.0-beta.2](https://github.com/JairajJangle/react-native-visibility-sensor/compare/v1.4.0-beta.1...v1.4.0-beta.2) (2025-08-23)
+
+
+### Bug Fixes
+
+* added missing state dependencies in visiblity calculations ([af0161c](https://github.com/JairajJangle/react-native-visibility-sensor/commit/af0161c6fcfb4d72f7db97e7fb1c88cc96706c13))
+
+# [1.4.0-beta.1](https://github.com/JairajJangle/react-native-visibility-sensor/compare/v1.3.22...v1.4.0-beta.1) (2025-08-23)
+
+
+### Bug Fixes
+
+* add window dimension change listener to handle orientation changes ([5f640db](https://github.com/JairajJangle/react-native-visibility-sensor/commit/5f640db1be9f447f9b82aece4b1e9a366df36573))
+* handle initial visibility state and improve measurement timing ([f5e4de7](https://github.com/JairajJangle/react-native-visibility-sensor/commit/f5e4de7dd7802951a5654fe9ab8cf574f9f4d52d))
+* prevent race conditions during rapid mount/unmount cycles ([de6cb59](https://github.com/JairajJangle/react-native-visibility-sensor/commit/de6cb59111b4b8944d3070bb826290b8941340c5))
+* prevent state updates on unmounted components ([6c9786a](https://github.com/JairajJangle/react-native-visibility-sensor/commit/6c9786a1ccc46e5c48813e349a7e6aabb9b4052a))
+
+
+### Features
+
+* added percent visiblity callback requested in [#44](https://github.com/JairajJangle/react-native-visibility-sensor/issues/44) ([e78fa27](https://github.com/JairajJangle/react-native-visibility-sensor/commit/e78fa27a9fe2d8c37227f3ab6022964aeafe8b37))
+
+
+### Performance Improvements
+
+* conserved percent calc. if view is not visible - [#44](https://github.com/JairajJangle/react-native-visibility-sensor/issues/44) ([81d0036](https://github.com/JairajJangle/react-native-visibility-sensor/commit/81d003651348dcfb871d832137330d174f1f56e2))
+
## [1.3.22](https://github.com/JairajJangle/react-native-visibility-sensor/compare/v1.3.21...v1.3.22) (2025-08-12)
diff --git a/README.md b/README.md
index 2334b5f..e7e6def 100644
--- a/README.md
+++ b/README.md
@@ -5,10 +5,11 @@
[](https://badge.fury.io/js/%40futurejj%2Freact-native-visibility-sensor) [](https://github.com/JairajJangle/react-native-visibility-sensor/blob/main/LICENSE) [](https://github.com/JairajJangle/react-native-visibility-sensor/actions/workflows/ci.yml)    [](https://github.com/JairajJangle/react-native-visibility-sensor/issues?q=is%3Aopen+is%3Aissue)  [](https://snack.expo.dev/@futurejj/react-native-visibility-sensor-example)  
-

+
+
## Installation
Using yarn
@@ -26,32 +27,28 @@ npm install @futurejj/react-native-visibility-sensor
## Usage
-```typescript
-import React from 'react';
+```tsx
+import React, { useState } from 'react';
import { ScrollView, Text } from 'react-native';
import { VisibilitySensor } from '@futurejj/react-native-visibility-sensor';
export default function VisibilitySensorExample() {
- const [isInView, setIsInView] = React.useState(false);
-
- function checkVisible(isVisible: boolean) {
- if (isVisible) {
- setIsInView(isVisible);
- } else {
- setIsInView(isVisible);
- }
- }
+ const [isVisible, setIsVisible] = useState(false);
+ const [percentVisible, setPercentVisible] = useState(0);
return (
checkVisible(isVisible)}
- threshold={{
- left: 100,
- right: 100,
- }}
- style={[styles.item, { backgroundColor: isInView ? 'green' : 'red' }]}>
- This View is currently visible? {isInView ? 'yes' : 'no'}
+ onChange={setIsVisible}
+ onPercentChange={setPercentVisible} // optional callback for % change
+ threshold={{ top: 100, bottom: 100 }}
+ style={[styles.item, { backgroundColor: isVisible ? 'green' : 'red' }]}>
+
+ {/* Visibility state */}
+ This View is currently visible? {isVisible ? 'yes' : 'no'}
+
+ {/* Percent visibility state */}
+ {`Percent visible: ${percentVisible}%`}
);
@@ -61,13 +58,14 @@ export default function VisibilitySensorExample() {
`VisibilitySensorProps` extends `ViewProps` from React Native, which includes common properties for all views, such as `style`, `onLayout`, etc.
-| Property | Type | Required | Description |
-| ----------- | ------------------------------------------------------- | -------- | ------------------------------------------------------------ |
-| onChange | (visible: boolean) => void | Yes | Callback function that fires when visibility changes. |
-| disabled | boolean | No | If `true`, disables the sensor. |
-| triggerOnce | boolean | No | If `true`, the sensor will only trigger once. |
-| delay | number \| undefined | No | The delay in milliseconds before the sensor triggers. |
-| threshold | [VisibilitySensorThreshold](#visibilitysensorthreshold) | No | Defines the part of the view that must be visible for the sensor to trigger. |
+| Property | Type | Required | Description |
+| ----------------- | ------------------------------------------------------- | -------- | ------------------------------------------------------------ |
+| `onChange` | `(visible: boolean) => void` | Yes | Callback function that fires when visibility changes. |
+| `onPercentChange` | `(percentVisible: number) => void` | No | Callback function that fires when visibility % changes. |
+| `disabled` | `boolean` | No | If `true`, disables the sensor. |
+| `triggerOnce` | `boolean` | No | If `true`, the sensor will only trigger once. |
+| `delay` | `number` or `undefined` | No | The delay in milliseconds before the sensor triggers. |
+| `threshold` | [VisibilitySensorThreshold](#visibilitysensorthreshold) | No | Defines the part of the view that must be visible for the sensor to trigger. |
Additionally, all properties from `ViewProps` are also applicable.
@@ -75,12 +73,12 @@ Additionally, all properties from `ViewProps` are also applicable.
### VisibilitySensorThreshold
-| Property | Type | Required | Description |
-| -------- | ------ | -------- | ------------------------------------------ |
-| top | number | No | The top threshold value for visibility. |
-| bottom | number | No | The bottom threshold value for visibility. |
-| left | number | No | The left threshold value for visibility. |
-| right | number | No | The right threshold value for visibility. |
+| Property | Type | Required | Description |
+| -------- | -------- | -------- | ------------------------------------------ |
+| `top` | `number` | No | The top threshold value for visibility. |
+| `bottom` | `number` | No | The bottom threshold value for visibility. |
+| `left` | `number` | No | The left threshold value for visibility. |
+| `right` | `number` | No | The right threshold value for visibility. |
---
diff --git a/example/package.json b/example/package.json
index f5b1da2..5ea462a 100644
--- a/example/package.json
+++ b/example/package.json
@@ -9,10 +9,10 @@
"web": "expo start --web"
},
"dependencies": {
- "expo": "^53.0.20",
+ "expo": "^53.0.22",
"react": "19.0.0",
"react-dom": "19.0.0",
- "react-native": "0.79.3",
+ "react-native": "0.79.5",
"react-native-web": "^0.20.0"
},
"devDependencies": {
diff --git a/example/src/App.tsx b/example/src/App.tsx
index be5e0d8..341ed37 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,8 +1,7 @@
-import * as React from 'react';
+import React, { useEffect, useState } from 'react';
import {
FlatList,
Image,
- SafeAreaView,
StyleSheet,
Text,
View,
@@ -10,17 +9,19 @@ import {
} from 'react-native';
import { VisibilitySensor } from '@futurejj/react-native-visibility-sensor';
import { pocketMonsters, type PocketMonsterInfo } from './pocketMonsters';
-import { useCallback, useState } from 'react';
+
+const topThreshold = 200;
+const bottomThreshold = 200;
export default function App() {
const renderItems: ListRenderItem = ({ item }) => (
-
+
);
return (
-
+ <>
index.toString()}
@@ -29,71 +30,121 @@ export default function App() {
ListHeaderComponent={}
ListFooterComponent={}
/>
-
+
+ {/* Threshold indicators - just to visualize the threshold boundaries */}
+ {/* Top */}
+
+ {/* Bottom */}
+
+ >
);
}
-const InViewPocketMonster = ({
+const PocketMonsterView = ({
name,
spriteUri,
}: {
name: string;
spriteUri: string;
}) => {
- const [isInView, setIsInView] = useState(false);
+ const [isVisible, setIsVisible] = useState(false);
+ const [percentVisible, setPercentVisible] = useState(0);
- const checkVisible = useCallback(
- (isVisible: boolean) => {
- setIsInView(isVisible);
+ useEffect(() => {
+ console.log(
+ // eslint-disable-next-line prettier/prettier
+ `${name} ${isVisible ? 'is now' : "isn't"} visible ${isVisible && !isVisible ? 'anymore' : ''
+ }`
+ );
+ }, [name, isVisible]);
- console.log(
- // eslint-disable-next-line prettier/prettier
- `${name} ${isVisible ? 'is now' : "isn't"} visible ${isInView && !isVisible ? 'anymore' : ''
- }`
- );
- },
- [name, isInView]
- );
return (
+ {/* Header percent visible text - to see % when the bottom is out of viewport */}
+
+ Percent Visible: {percentVisible}
+
+
+ {/* Image */}
+ {/* Name */}
{name}
+
+ {/* Footer percent visible text - to see % when the top is out of viewport */}
+
+ Percent Visible: {percentVisible}
+
);
};
const styles = StyleSheet.create({
- container: { flex: 1 },
+ container: {
+ flex: 1,
+ },
flatListContentContainer: {
alignItems: 'center',
},
- spacer: { marginHorizontal: 35 },
+ spacer: {
+ marginHorizontal: 35,
+ },
+
+ thresholdIndicatorBase: {
+ position: 'absolute',
+ width: '100%',
+ alignItems: 'center',
+ zIndex: 9999,
+ backgroundColor: 'rgba(255, 0, 0, 0.2)',
+ },
+ topThresholdIndicator: {
+ top: 0,
+ height: topThreshold,
+ },
+ bottomThresholdIndicator: {
+ bottom: 0,
+ height: bottomThreshold,
+ },
+
pocketMonster: {
width: 200,
height: 200,
resizeMode: 'contain',
},
- pocketMonsterInView: {
+ pocketMonsterVisible: {
opacity: 1,
},
- pocketMonsterNotInView: {
+ pocketMonsterNotVisible: {
opacity: 0.4,
},
pocketMonsterName: {
@@ -102,22 +153,34 @@ const styles = StyleSheet.create({
marginTop: 16,
fontSize: 26,
},
+
+ pocketMonstersPercentVisibilityTextBase: {
+ textAlign: 'center',
+ fontSize: 16,
+ },
+ pocketMonsterPercentVisibilityHeader: {
+ marginBottom: 10,
+ },
+ pocketMonsterPercentVisibilityFooter: {
+ marginTop: 10,
+ },
+
listHeaderFooter: {
- padding: 20,
+ padding: 40,
},
visibilitySensor: {
marginBottom: 20,
- width: 300,
- height: 300,
+ width: 320,
+ height: 320,
borderRadius: 20,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
- inView: {
+ visible: {
backgroundColor: 'yellow',
},
- notInView: {
+ notVisible: {
backgroundColor: 'pink',
opacity: 0.8,
},
diff --git a/example/yarn.lock b/example/yarn.lock
index 4833216..3d61cbe 100644
--- a/example/yarn.lock
+++ b/example/yarn.lock
@@ -764,10 +764,10 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
-"@expo/cli@0.24.20":
- version "0.24.20"
- resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.24.20.tgz#c9a3ad7eb93b0d6a39da20ef8804976b65838790"
- integrity sha512-uF1pOVcd+xizNtVTuZqNGzy7I6IJon5YMmQidsURds1Ww96AFDxrR/NEACqeATNAmY60m8wy1VZZpSg5zLNkpw==
+"@expo/cli@0.24.21":
+ version "0.24.21"
+ resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.24.21.tgz#16090692059c24d55324060997510cf9e039f9f4"
+ integrity sha512-DT6K9vgFHqqWL/19mU1ofRcPoO1pn4qmgi76GtuiNU4tbBe/02mRHwFsQw7qRfFAT28If5e/wiwVozgSuZVL8g==
dependencies:
"@0no-co/graphql.web" "^1.0.8"
"@babel/runtime" "^7.20.0"
@@ -783,10 +783,11 @@
"@expo/package-manager" "^1.8.6"
"@expo/plist" "^0.3.5"
"@expo/prebuild-config" "^9.0.11"
+ "@expo/schema-utils" "^0.1.0"
"@expo/spawn-async" "^1.7.2"
"@expo/ws-tunnel" "^1.0.1"
"@expo/xcpretty" "^4.3.0"
- "@react-native/dev-middleware" "0.79.5"
+ "@react-native/dev-middleware" "0.79.6"
"@urql/core" "^5.0.6"
"@urql/exchange-retry" "^1.3.0"
accepts "^1.3.8"
@@ -1031,6 +1032,11 @@
semver "^7.6.0"
xml2js "0.6.0"
+"@expo/schema-utils@^0.1.0":
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/@expo/schema-utils/-/schema-utils-0.1.0.tgz#3f7dcfb6c32a03c5535d4748f1fa37f836cd903a"
+ integrity sha512-Me2avOfbcVT/O5iRmPKLCCSvbCfVfxIstGMlzVJOffplaZX1+ut8D18siR1wx5fkLMTWKs14ozEz11cGUY7hcw==
+
"@expo/sdk-runtime-versions@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz#d7ebd21b19f1c6b0395e50d78da4416941c57f7c"
@@ -1284,23 +1290,23 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
-"@react-native/assets-registry@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.79.3.tgz#022218d55a5d9d221a6d176987ab0b35c10d388b"
- integrity sha512-Vy8DQXCJ21YSAiHxrNBz35VqVlZPpRYm50xRTWRf660JwHuJkFQG8cUkrLzm7AUriqUXxwpkQHcY+b0ibw9ejQ==
-
-"@react-native/babel-plugin-codegen@0.79.5":
+"@react-native/assets-registry@0.79.5":
version "0.79.5"
- resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.5.tgz#a2a9fd04fbb28ac75694952c234b159de25b2c52"
- integrity sha512-Rt/imdfqXihD/sn0xnV4flxxb1aLLjPtMF1QleQjEhJsTUPpH4TFlfOpoCvsrXoDl4OIcB1k4FVM24Ez92zf5w==
+ resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.79.5.tgz#90a178ec6646a22eb4218285cc2df7fd82603e34"
+ integrity sha512-N4Kt1cKxO5zgM/BLiyzuuDNquZPiIgfktEQ6TqJ/4nKA8zr4e8KJgU6Tb2eleihDO4E24HmkvGc73naybKRz/w==
+
+"@react-native/babel-plugin-codegen@0.79.6":
+ version "0.79.6"
+ resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.6.tgz#2e86024a649072268b03b28da8555f9c81bdb51b"
+ integrity sha512-CS5OrgcMPixOyUJ/Sk/HSsKsKgyKT5P7y3CojimOQzWqRZBmoQfxdST4ugj7n1H+ebM2IKqbgovApFbqXsoX0g==
dependencies:
"@babel/traverse" "^7.25.3"
- "@react-native/codegen" "0.79.5"
+ "@react-native/codegen" "0.79.6"
-"@react-native/babel-preset@0.79.5":
- version "0.79.5"
- resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.79.5.tgz#2af2d055d4e67c321bf32744b85917490132992b"
- integrity sha512-GDUYIWslMLbdJHEgKNfrOzXk8EDKxKzbwmBXUugoiSlr6TyepVZsj3GZDLEFarOcTwH1EXXHJsixihk8DCRQDA==
+"@react-native/babel-preset@0.79.6":
+ version "0.79.6"
+ resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.79.6.tgz#bc0e94a0b3403d237a60902161587ff90205835c"
+ integrity sha512-H+FRO+r2Ql6b5IwfE0E7D52JhkxjeGSBSUpCXAI5zQ60zSBJ54Hwh2bBJOohXWl4J+C7gKYSAd2JHMUETu+c/A==
dependencies:
"@babel/core" "^7.25.2"
"@babel/plugin-proposal-export-default-from" "^7.24.7"
@@ -1343,15 +1349,15 @@
"@babel/plugin-transform-typescript" "^7.25.2"
"@babel/plugin-transform-unicode-regex" "^7.24.7"
"@babel/template" "^7.25.0"
- "@react-native/babel-plugin-codegen" "0.79.5"
+ "@react-native/babel-plugin-codegen" "0.79.6"
babel-plugin-syntax-hermes-parser "0.25.1"
babel-plugin-transform-flow-enums "^0.0.2"
react-refresh "^0.14.0"
-"@react-native/codegen@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.79.3.tgz#49689132718c81a3b25426769bc6fd8fd2a0469f"
- integrity sha512-CZejXqKch/a5/s/MO5T8mkAgvzCXgsTkQtpCF15kWR9HN8T+16k0CsN7TXAxXycltoxiE3XRglOrZNEa/TiZUQ==
+"@react-native/codegen@0.79.5":
+ version "0.79.5"
+ resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.79.5.tgz#f0f1f82b2603959b8e23711b55eac3dab6490596"
+ integrity sha512-FO5U1R525A1IFpJjy+KVznEinAgcs3u7IbnbRJUG9IH/MBXi2lEU2LtN+JarJ81MCfW4V2p0pg6t/3RGHFRrlQ==
dependencies:
glob "^7.1.1"
hermes-parser "0.25.1"
@@ -1359,23 +1365,25 @@
nullthrows "^1.1.1"
yargs "^17.6.2"
-"@react-native/codegen@0.79.5":
- version "0.79.5"
- resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.79.5.tgz#f0f1f82b2603959b8e23711b55eac3dab6490596"
- integrity sha512-FO5U1R525A1IFpJjy+KVznEinAgcs3u7IbnbRJUG9IH/MBXi2lEU2LtN+JarJ81MCfW4V2p0pg6t/3RGHFRrlQ==
+"@react-native/codegen@0.79.6":
+ version "0.79.6"
+ resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.79.6.tgz#25e9bb68ce02afcdb01b9b2b0bf8a3a7fd99bf8b"
+ integrity sha512-iRBX8Lgbqypwnfba7s6opeUwVyaR23mowh9ILw7EcT2oLz3RqMmjJdrbVpWhGSMGq2qkPfqAH7bhO8C7O+xfjQ==
dependencies:
+ "@babel/core" "^7.25.2"
+ "@babel/parser" "^7.25.3"
glob "^7.1.1"
hermes-parser "0.25.1"
invariant "^2.2.4"
nullthrows "^1.1.1"
yargs "^17.6.2"
-"@react-native/community-cli-plugin@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.3.tgz#84821d3401074e036ba05b8b6ca1ee122cb43e29"
- integrity sha512-N/+p4HQqN4yK6IRzn7OgMvUIcrmEWkecglk1q5nj+AzNpfIOzB+mqR20SYmnPfeXF+mZzYCzRANb3KiM+WsSDA==
+"@react-native/community-cli-plugin@0.79.5":
+ version "0.79.5"
+ resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.5.tgz#1cf71637f575a322cdcf6f8b5aeb928aed842508"
+ integrity sha512-ApLO1ARS8JnQglqS3JAHk0jrvB+zNW3dvNJyXPZPoygBpZVbf8sjvqeBiaEYpn8ETbFWddebC4HoQelDndnrrA==
dependencies:
- "@react-native/dev-middleware" "0.79.3"
+ "@react-native/dev-middleware" "0.79.5"
chalk "^4.0.0"
debug "^2.2.0"
invariant "^2.2.4"
@@ -1384,23 +1392,23 @@
metro-core "^0.82.0"
semver "^7.1.3"
-"@react-native/debugger-frontend@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.79.3.tgz#9cb57d8e88c22552194ab5f6f257605b151bc5b3"
- integrity sha512-ImNDuEeKH6lEsLXms3ZsgIrNF94jymfuhPcVY5L0trzaYNo9ZFE9Ni2/18E1IbfXxdeIHrCSBJlWD6CTm7wu5A==
-
"@react-native/debugger-frontend@0.79.5":
version "0.79.5"
resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.79.5.tgz#76b8d77b62003b4ea99354fe435c01d727b64584"
integrity sha512-WQ49TRpCwhgUYo5/n+6GGykXmnumpOkl4Lr2l2o2buWU9qPOwoiBqJAtmWEXsAug4ciw3eLiVfthn5ufs0VB0A==
-"@react-native/dev-middleware@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.79.3.tgz#3e315ef7516ebad60a4202b4094d84fedecb4064"
- integrity sha512-x88+RGOyG71+idQefnQg7wLhzjn/Scs+re1O5vqCkTVzRAc/f7SdHMlbmECUxJPd08FqMcOJr7/X3nsJBrNuuw==
+"@react-native/debugger-frontend@0.79.6":
+ version "0.79.6"
+ resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.79.6.tgz#ec0ea9c2f140a564d26789a18dc097519f1b9c48"
+ integrity sha512-lIK/KkaH7ueM22bLO0YNaQwZbT/oeqhaghOvmZacaNVbJR1Cdh/XAqjT8FgCS+7PUnbxA8B55NYNKGZG3O2pYw==
+
+"@react-native/dev-middleware@0.79.5":
+ version "0.79.5"
+ resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.79.5.tgz#8c7b2b790943f24e33a21da39a7c3959ea93304b"
+ integrity sha512-U7r9M/SEktOCP/0uS6jXMHmYjj4ESfYCkNAenBjFjjsRWekiHE+U/vRMeO+fG9gq4UCcBAUISClkQCowlftYBw==
dependencies:
"@isaacs/ttlcache" "^1.4.1"
- "@react-native/debugger-frontend" "0.79.3"
+ "@react-native/debugger-frontend" "0.79.5"
chrome-launcher "^0.15.2"
chromium-edge-launcher "^0.2.0"
connect "^3.6.5"
@@ -1411,13 +1419,13 @@
serve-static "^1.16.2"
ws "^6.2.3"
-"@react-native/dev-middleware@0.79.5":
- version "0.79.5"
- resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.79.5.tgz#8c7b2b790943f24e33a21da39a7c3959ea93304b"
- integrity sha512-U7r9M/SEktOCP/0uS6jXMHmYjj4ESfYCkNAenBjFjjsRWekiHE+U/vRMeO+fG9gq4UCcBAUISClkQCowlftYBw==
+"@react-native/dev-middleware@0.79.6":
+ version "0.79.6"
+ resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.79.6.tgz#62a4c0b987e5d100eae3e8c95c58ae1c8abe377a"
+ integrity sha512-BK3GZBa9c7XSNR27EDRtxrgyyA3/mf1j3/y+mPk7Ac0Myu85YNrXnC9g3mL5Ytwo0g58TKrAIgs1fF2Q5Mn6mQ==
dependencies:
"@isaacs/ttlcache" "^1.4.1"
- "@react-native/debugger-frontend" "0.79.5"
+ "@react-native/debugger-frontend" "0.79.6"
chrome-launcher "^0.15.2"
chromium-edge-launcher "^0.2.0"
connect "^3.6.5"
@@ -1428,20 +1436,15 @@
serve-static "^1.16.2"
ws "^6.2.3"
-"@react-native/gradle-plugin@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.79.3.tgz#69ba47ac406ccdb3b3829f311bd7c27e6fad7ebc"
- integrity sha512-imfpZLhNBc9UFSzb/MOy2tNcIBHqVmexh/qdzw83F75BmUtLb/Gs1L2V5gw+WI1r7RqDILbWk7gXB8zUllwd+g==
-
-"@react-native/js-polyfills@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.79.3.tgz#bf5614363f118c6bdf2f773c578e603c88d0425c"
- integrity sha512-PEBtg6Kox6KahjCAch0UrqCAmHiNLEbp2SblUEoFAQnov4DSxBN9safh+QSVaCiMAwLjvNfXrJyygZz60Dqz3Q==
+"@react-native/gradle-plugin@0.79.5":
+ version "0.79.5"
+ resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.79.5.tgz#c2dbdf17a2b724b8f4442a01613c847564503813"
+ integrity sha512-K3QhfFNKiWKF3HsCZCEoWwJPSMcPJQaeqOmzFP4RL8L3nkpgUwn74PfSCcKHxooVpS6bMvJFQOz7ggUZtNVT+A==
-"@react-native/normalize-colors@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.79.3.tgz#e491937436a2c287707e24263308c818a66eb447"
- integrity sha512-T75NIQPRFCj6DFMxtcVMJTZR+3vHXaUMSd15t+CkJpc5LnyX91GVaPxpRSAdjFh7m3Yppl5MpdjV/fntImheYQ==
+"@react-native/js-polyfills@0.79.5":
+ version "0.79.5"
+ resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.79.5.tgz#61b6c43832b644669d1f00dbbaa51a079c5b9b4c"
+ integrity sha512-a2wsFlIhvd9ZqCD5KPRsbCQmbZi6KxhRN++jrqG0FUTEV5vY7MvjjUqDILwJd2ZBZsf7uiDuClCcKqA+EEdbvw==
"@react-native/normalize-colors@0.79.5":
version "0.79.5"
@@ -1453,10 +1456,10 @@
resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz#b8ac17d1bbccd3ef9a1f921665d04d42cff85976"
integrity sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==
-"@react-native/virtualized-lists@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.79.3.tgz#4a2799017cd3795f519422f48b3c0bbc4739a245"
- integrity sha512-/0rRozkn+iIHya2vnnvprDgT7QkfI54FLrACAN3BLP7MRlfOIGOrZsXpRLndnLBVnjNzkcre84i1RecjoXnwIA==
+"@react-native/virtualized-lists@0.79.5":
+ version "0.79.5"
+ resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.79.5.tgz#5dbc01dcb4c836d40edcb4034b240a300ee310fb"
+ integrity sha512-EUPM2rfGNO4cbI3olAbhPkIt3q7MapwCwAJBzUfWlZ/pu0PRNOnMQ1IvaXTf3TpeozXV52K1OdprLEI/kI5eUA==
dependencies:
invariant "^2.2.4"
nullthrows "^1.1.1"
@@ -2221,10 +2224,10 @@ babel-preset-current-node-syntax@^1.0.0:
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
"@babel/plugin-syntax-top-level-await" "^7.14.5"
-babel-preset-expo@~13.2.3:
- version "13.2.3"
- resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-13.2.3.tgz#d96d2df314a9e20d5218a0642e6541c4735f3a34"
- integrity sha512-wQJn92lqj8GKR7Ojg/aW4+GkqI6ZdDNTDyOqhhl7A9bAqk6t0ukUOWLDXQb4p0qKJjMDV1F6gNWasI2KUbuVTQ==
+babel-preset-expo@~13.2.4:
+ version "13.2.4"
+ resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-13.2.4.tgz#ad31bbfc8b3169a5a61108cebdee5350feebc071"
+ integrity sha512-3IKORo3KR+4qtLdCkZNDj8KeA43oBn7RRQejFGWfiZgu/NeaRUSri8YwYjZqybm7hn3nmMv9OLahlvXBX23o5Q==
dependencies:
"@babel/helper-module-imports" "^7.25.9"
"@babel/plugin-proposal-decorators" "^7.12.9"
@@ -2240,7 +2243,7 @@ babel-preset-expo@~13.2.3:
"@babel/plugin-transform-runtime" "^7.24.7"
"@babel/preset-react" "^7.22.15"
"@babel/preset-typescript" "^7.23.0"
- "@react-native/babel-preset" "0.79.5"
+ "@react-native/babel-preset" "0.79.6"
babel-plugin-react-native-web "~0.19.13"
babel-plugin-syntax-hermes-parser "^0.25.1"
babel-plugin-transform-flow-enums "^0.0.2"
@@ -3339,19 +3342,19 @@ expo-pwa@0.0.127:
commander "2.20.0"
update-check "1.5.3"
-expo@^53.0.20:
- version "53.0.20"
- resolved "https://registry.yarnpkg.com/expo/-/expo-53.0.20.tgz#e11b553322de313d47c407367d8d48d3a073e3cc"
- integrity sha512-Nh+HIywVy9KxT/LtH08QcXqrxtUOA9BZhsXn3KCsAYA+kNb80M8VKN8/jfQF+I6CgeKyFKJoPNsWgI0y0VBGrA==
+expo@^53.0.22:
+ version "53.0.22"
+ resolved "https://registry.yarnpkg.com/expo/-/expo-53.0.22.tgz#ff61b6bcdf0855b7b88ca5ca0f622e12cbdb1d0f"
+ integrity sha512-sJ2I4W/e5iiM4u/wYCe3qmW4D7WPCRqByPDD0hJcdYNdjc9HFFFdO4OAudZVyC/MmtoWZEIH5kTJP1cw9FjzYA==
dependencies:
"@babel/runtime" "^7.20.0"
- "@expo/cli" "0.24.20"
+ "@expo/cli" "0.24.21"
"@expo/config" "~11.0.13"
"@expo/config-plugins" "~10.1.2"
"@expo/fingerprint" "0.13.4"
"@expo/metro-config" "0.20.17"
"@expo/vector-icons" "^14.0.0"
- babel-preset-expo "~13.2.3"
+ babel-preset-expo "~13.2.4"
expo-asset "~11.1.7"
expo-constants "~17.1.7"
expo-file-system "~18.1.11"
@@ -5809,19 +5812,19 @@ react-native-web@^0.20.0:
postcss-value-parser "^4.2.0"
styleq "^0.1.3"
-react-native@0.79.3:
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.79.3.tgz#16580ca202016c75e3c61116fcfe3b30f6d762fc"
- integrity sha512-EzH1+9gzdyEo9zdP6u7Sh3Jtf5EOMwzy+TK65JysdlgAzfEVfq4mNeXcAZ6SmD+CW6M7ARJbvXLyTD0l2S5rpg==
+react-native@0.79.5:
+ version "0.79.5"
+ resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.79.5.tgz#a91cd92bb282a4f8420fdd64fe3a9434580404b2"
+ integrity sha512-jVihwsE4mWEHZ9HkO1J2eUZSwHyDByZOqthwnGrVZCh6kTQBCm4v8dicsyDa6p0fpWNE5KicTcpX/XXl0ASJFg==
dependencies:
"@jest/create-cache-key-function" "^29.7.0"
- "@react-native/assets-registry" "0.79.3"
- "@react-native/codegen" "0.79.3"
- "@react-native/community-cli-plugin" "0.79.3"
- "@react-native/gradle-plugin" "0.79.3"
- "@react-native/js-polyfills" "0.79.3"
- "@react-native/normalize-colors" "0.79.3"
- "@react-native/virtualized-lists" "0.79.3"
+ "@react-native/assets-registry" "0.79.5"
+ "@react-native/codegen" "0.79.5"
+ "@react-native/community-cli-plugin" "0.79.5"
+ "@react-native/gradle-plugin" "0.79.5"
+ "@react-native/js-polyfills" "0.79.5"
+ "@react-native/normalize-colors" "0.79.5"
+ "@react-native/virtualized-lists" "0.79.5"
abort-controller "^3.0.0"
anser "^1.4.9"
ansi-regex "^5.0.0"
diff --git a/package.json b/package.json
index 49c7c18..f09955c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@futurejj/react-native-visibility-sensor",
- "version": "1.3.22",
+ "version": "1.4.0-beta.3",
"description": "A React Native wrapper to check whether a component is in the view port to track impressions and clicks",
"main": "lib/commonjs/index",
"module": "lib/module/index",
diff --git a/release.config.js b/release.config.js
index bf74e46..7b6185e 100644
--- a/release.config.js
+++ b/release.config.js
@@ -1,10 +1,35 @@
module.exports = {
- branches: ['main'],
+ branches: [
+ 'main',
+ {
+ name: 'beta',
+ prerelease: true, // Marks this as a prerelease channel
+ },
+ ],
plugins: [
- '@semantic-release/commit-analyzer', // Analyzes commits for version bumping
+ [
+ '@semantic-release/commit-analyzer',
+ {
+ preset: 'angular',
+ releaseRules: [
+ { type: 'chore', scope: 'deps', release: 'patch' },
+ { type: 'chore', breaking: true, release: 'major' },
+ { type: 'chore', scope: 'breaking', release: 'major' },
+ ],
+ parserOpts: {
+ noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES'],
+ },
+ },
+ ],
'@semantic-release/release-notes-generator', // Generates release notes
'@semantic-release/changelog', // Generates the changelog
- '@semantic-release/npm', // Handles npm publishing
+ [
+ '@semantic-release/npm',
+ {
+ npmPublish: true,
+ tag: 'beta', // Publishes with a 'beta' tag to npm
+ },
+ ],
'@semantic-release/github', // Handles GitHub releases
[
'@semantic-release/git',
diff --git a/src/VisibilitySensor.tsx b/src/VisibilitySensor.tsx
index 2d48b72..3afeba2 100644
--- a/src/VisibilitySensor.tsx
+++ b/src/VisibilitySensor.tsx
@@ -6,13 +6,19 @@ import React, {
forwardRef,
useImperativeHandle,
} from 'react';
-import { Dimensions, type ScaledSize, View } from 'react-native';
+import { useWindowDimensions, View } from 'react-native';
import type {
VisibilitySensorRef,
VisibilitySensorProps,
RectDimensionsState,
} from './visibilitySensor.types';
+enum MeasurementState {
+ IDLE = 'IDLE', // Not yet measured
+ MEASURING = 'MEASURING', // Measurement in progress
+ MEASURED = 'MEASURED', // Has valid measurements
+}
+
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
@@ -34,6 +40,7 @@ const VisibilitySensor = forwardRef(
(props, ref) => {
const {
onChange,
+ onPercentChange,
disabled = false,
triggerOnce = false,
delay,
@@ -42,12 +49,17 @@ const VisibilitySensor = forwardRef(
...rest
} = props;
- const localRef = useRef(null);
-
useImperativeHandle(ref, () => ({
getInnerRef: () => localRef.current,
}));
+ const window = useWindowDimensions();
+
+ const localRef = useRef(null);
+ const isMountedRef = useRef(true);
+ const measurementStateRef = useRef(MeasurementState.IDLE);
+ const lastPercentRef = useRef(undefined);
+
const [rectDimensions, setRectDimensions] = useState({
rectTop: 0,
rectBottom: 0,
@@ -58,12 +70,19 @@ const VisibilitySensor = forwardRef(
});
const [lastValue, setLastValue] = useState(undefined);
const [active, setActive] = useState(false);
- const hasMeasuredRef = useRef(false);
- const measureInnerView = () => {
+ const measureInnerView = useCallback(() => {
/* Check if the sensor is active to prevent unnecessary measurements
This avoids running measurements when the sensor is disabled or stopped */
- if (!active) return;
+ if (
+ !active ||
+ !isMountedRef.current ||
+ measurementStateRef.current === MeasurementState.MEASURING
+ ) {
+ return;
+ }
+
+ measurementStateRef.current = MeasurementState.MEASURING;
localRef.current?.measure(
(
@@ -74,6 +93,11 @@ const VisibilitySensor = forwardRef(
pageX: number,
pageY: number
) => {
+ // Check if component is still mounted before setting state because measurement can be asynchronous
+ if (!isMountedRef.current) {
+ return;
+ }
+
const dimensions = {
rectTop: pageY,
rectBottom: pageY + height,
@@ -91,13 +115,14 @@ const VisibilitySensor = forwardRef(
rectDimensions.rectHeight !== dimensions.rectHeight
) {
setRectDimensions(dimensions);
- /* Set hasMeasuredRef to true to indicate that a valid measurement has been taken
- This ensures visibility checks only proceed after initial measurement */
- hasMeasuredRef.current = true;
}
+
+ /* Set measurementStateRef to MEASURED to indicate that a valid measurement has
+ been taken. This ensures visibility checks only proceed after initial measurement */
+ measurementStateRef.current = MeasurementState.MEASURED;
}
);
- };
+ }, [active, rectDimensions]);
useInterval(measureInnerView, delay || 100);
@@ -110,10 +135,44 @@ const VisibilitySensor = forwardRef(
setActive(false);
/* Reset measurement state when stopping to ensure fresh measurements
when the sensor is reactivated */
- hasMeasuredRef.current = false;
+ measurementStateRef.current = MeasurementState.IDLE; // Reset state
}
}, [active]);
+ // Effect to trigger initial measurement when component becomes active:
+ useEffect(() => {
+ let timer: ReturnType;
+
+ if (active && measurementStateRef.current === MeasurementState.IDLE) {
+ // Use setTimeout with 0 delay to ensure layout is complete
+ timer = setTimeout(() => {
+ measureInnerView();
+ }, 0);
+ }
+
+ return () => {
+ if (timer) clearTimeout(timer);
+ };
+ }, [active, measureInnerView]);
+
+ // Reset measurement state when dimensions change:
+ useEffect(() => {
+ if (
+ isMountedRef.current &&
+ measurementStateRef.current === MeasurementState.MEASURED
+ ) {
+ // Reset measurement state to force remeasurement with new dimensions
+ measurementStateRef.current = MeasurementState.IDLE;
+ }
+ }, [window]);
+
+ useEffect(() => {
+ isMountedRef.current = true;
+ return () => {
+ isMountedRef.current = false;
+ };
+ }, []);
+
useEffect(() => {
if (!disabled) {
startWatching();
@@ -128,15 +187,70 @@ const VisibilitySensor = forwardRef(
/* Ensure visibility checks only run when the sensor is active and
at least one measurement has been completed. This prevents
premature visibility calculations with invalid or stale dimensions */
- if (!active || !hasMeasuredRef.current) return;
+ if (
+ !active ||
+ measurementStateRef.current !== MeasurementState.MEASURED ||
+ !isMountedRef.current
+ ) {
+ return;
+ }
- const window: ScaledSize = Dimensions.get('window');
const isVisible: boolean =
rectDimensions.rectTop + (threshold.top || 0) <= window.height && // Top edge is within the bottom of the window
rectDimensions.rectBottom - (threshold.bottom || 0) >= 0 && // Bottom edge is within the top of the window
rectDimensions.rectLeft + (threshold.left || 0) <= window.width && // Left edge is within the right of the window
rectDimensions.rectRight - (threshold.right || 0) >= 0; // Right edge is within the left of the window
+ // Calculate percent visible if callback is requested / provided
+ if (
+ onPercentChange &&
+ rectDimensions.rectWidth > 0 &&
+ rectDimensions.rectHeight > 0
+ ) {
+ let percentVisible = 0;
+
+ // Don't perform % calculation if not visible for efficiency
+ if (isVisible) {
+ // Thresholds reduce the effective viewport
+ const viewportTop = 0 + (threshold.top || 0);
+ const viewportBottom = window.height - (threshold.bottom || 0);
+ const viewportLeft = 0 + (threshold.left || 0);
+ const viewportRight = window.width - (threshold.right || 0);
+
+ // Calculate the visible portion of the element within the reduced viewport
+ const visibleTop = Math.max(viewportTop, rectDimensions.rectTop);
+ const visibleBottom = Math.min(
+ viewportBottom,
+ rectDimensions.rectBottom
+ );
+ const visibleLeft = Math.max(viewportLeft, rectDimensions.rectLeft);
+ const visibleRight = Math.min(
+ viewportRight,
+ rectDimensions.rectRight
+ );
+
+ // Calculate visible dimensions
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
+ const visibleWidth = Math.max(0, visibleRight - visibleLeft);
+
+ // Calculate percent visible based on actual element area
+ const visibleArea = visibleHeight * visibleWidth;
+ const totalArea =
+ rectDimensions.rectHeight * rectDimensions.rectWidth;
+ percentVisible =
+ totalArea > 0 ? Math.round((visibleArea / totalArea) * 100) : 0;
+ } else {
+ // when !isVisible
+ percentVisible = 0; // No need to calculate, it's fully out of view, so 0%
+ }
+
+ // Only fire callback if percent has changed
+ if (lastPercentRef.current !== percentVisible) {
+ lastPercentRef.current = percentVisible; // Update last reported percent
+ onPercentChange(percentVisible);
+ }
+ }
+
if (lastValue !== isVisible) {
setLastValue(isVisible);
onChange(isVisible);
@@ -144,8 +258,17 @@ const VisibilitySensor = forwardRef(
stopWatching();
}
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [rectDimensions, lastValue, active]);
+ }, [
+ rectDimensions,
+ window,
+ lastValue,
+ active,
+ onPercentChange,
+ threshold,
+ onChange,
+ triggerOnce,
+ stopWatching,
+ ]);
return (
diff --git a/src/visibilitySensor.types.ts b/src/visibilitySensor.types.ts
index 4b9e791..2780d9e 100644
--- a/src/visibilitySensor.types.ts
+++ b/src/visibilitySensor.types.ts
@@ -2,6 +2,7 @@ import type { View, ViewProps } from 'react-native';
export interface VisibilitySensorProps extends ViewProps {
onChange: (visible: boolean) => void;
+ onPercentChange?: (percentVisible: number) => void;
disabled?: boolean;
triggerOnce?: boolean;
delay?: number | undefined;