diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 377e77b..97b5f6b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -48,11 +48,10 @@ jobs:
fail_ci_if_error: true
- name: Build package
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: pnpm build
- - name: Check package size
- run: pnpm size
-
- name: Package smoke
run: pnpm test:package-smoke
diff --git a/README.md b/README.md
index 7c8e6d3..d06a1bb 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# react-atom-trigger
[](https://codecov.io/gh/innrvoice/react-atom-trigger)
-[](https://bundlejs.com/?q=react-atom-trigger)
+[](https://app.codecov.io/github/innrvoice/react-atom-trigger/bundles/master/react-atom-trigger-esm)
`react-atom-trigger` helps with the usual "run some code when this thing enters or leaves view" problem.
It is a lightweight React alternative to `react-waypoint`, written in TypeScript.
diff --git a/package.json b/package.json
index cd80cbb..cefa03c 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,6 @@
"preview:sb": "pnpm build:sb && npx serve -s storybook-static -l 3000",
"start:storybook": "node scripts/serve-storybook.mjs",
"precommit:checks": "pnpm format:check && pnpm lint && pnpm test",
- "size": "size-limit",
"test": "pnpm test:all",
"test:all": "pnpm test:unit && pnpm test:storybook",
"test:coverage": "pnpm test:coverage:unit",
@@ -67,7 +66,7 @@
"build-storybook": "storybook build -o storybook-static"
},
"devDependencies": {
- "@size-limit/preset-small-lib": "^12.1.0",
+ "@codecov/rollup-plugin": "^1.9.1",
"@storybook/addon-docs": "10.3.4",
"@storybook/addon-vitest": "10.3.4",
"@storybook/react-vite": "10.3.4",
@@ -84,7 +83,6 @@
"playwright": "^1.59.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
- "size-limit": "^12.1.0",
"storybook": "10.3.4",
"tsdown": "^0.21.7",
"tslib": "^2.8.0",
@@ -95,12 +93,6 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
- "size-limit": [
- {
- "path": "lib/index.js",
- "limit": "5 kB"
- }
- ],
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"pnpm": {
"overrides": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5cac388..019602c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,9 +14,9 @@ importers:
.:
devDependencies:
- '@size-limit/preset-small-lib':
- specifier: ^12.1.0
- version: 12.1.0(size-limit@12.1.0)
+ '@codecov/rollup-plugin':
+ specifier: ^1.9.1
+ version: 1.9.1(rollup@4.60.1)
'@storybook/addon-docs':
specifier: 10.3.4
version: 10.3.4(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@6.4.2(@types/node@20.19.35))
@@ -65,9 +65,6 @@ importers:
react-dom:
specifier: ^19.2.4
version: 19.2.4(react@19.2.4)
- size-limit:
- specifier: ^12.1.0
- version: 12.1.0
storybook:
specifier: 10.3.4
version: 10.3.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -89,6 +86,21 @@ importers:
packages:
+ '@actions/core@1.11.1':
+ resolution: {integrity: sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==}
+
+ '@actions/exec@1.1.1':
+ resolution: {integrity: sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==}
+
+ '@actions/github@6.0.1':
+ resolution: {integrity: sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==}
+
+ '@actions/http-client@2.2.3':
+ resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==}
+
+ '@actions/io@1.1.3':
+ resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
+
'@adobe/css-tools@4.4.4':
resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
@@ -206,6 +218,16 @@ packages:
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
hasBin: true
+ '@codecov/bundler-plugin-core@1.9.1':
+ resolution: {integrity: sha512-dt3ic7gMswz4p/qdkYPVJwXlLiLsz55rBBn2I7mr0HTG8pCoLRqnANJIwo5WrqGBZgPyVSMPBqBra6VxLWfDyA==}
+ engines: {node: '>=18.0.0'}
+
+ '@codecov/rollup-plugin@1.9.1':
+ resolution: {integrity: sha512-osRI84VBvm8kL0/7w6FLTHsooYGkGbXp1KiVM8d3cH488bvR+E3au+09KxaLQtNNjcZXDGW5m8071EJLN7olAw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ rollup: 3.x || 4.x
+
'@csstools/color-helpers@6.0.2':
resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
engines: {node: '>=20.19.0'}
@@ -606,6 +628,54 @@ packages:
'@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
+ '@octokit/auth-token@4.0.0':
+ resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==}
+ engines: {node: '>= 18'}
+
+ '@octokit/core@5.2.2':
+ resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==}
+ engines: {node: '>= 18'}
+
+ '@octokit/endpoint@9.0.6':
+ resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==}
+ engines: {node: '>= 18'}
+
+ '@octokit/graphql@7.1.1':
+ resolution: {integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==}
+ engines: {node: '>= 18'}
+
+ '@octokit/openapi-types@20.0.0':
+ resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==}
+
+ '@octokit/openapi-types@24.2.0':
+ resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==}
+
+ '@octokit/plugin-paginate-rest@9.2.2':
+ resolution: {integrity: sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ '@octokit/core': '5'
+
+ '@octokit/plugin-rest-endpoint-methods@10.4.1':
+ resolution: {integrity: sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ '@octokit/core': '5'
+
+ '@octokit/request-error@5.1.1':
+ resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==}
+ engines: {node: '>= 18'}
+
+ '@octokit/request@8.4.1':
+ resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==}
+ engines: {node: '>= 18'}
+
+ '@octokit/types@12.6.0':
+ resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==}
+
+ '@octokit/types@13.10.0':
+ resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==}
+
'@oxc-project/types@0.122.0':
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
@@ -1104,23 +1174,6 @@ packages:
cpu: [x64]
os: [win32]
- '@size-limit/esbuild@12.1.0':
- resolution: {integrity: sha512-Um6MVrX+05kIxI4+zk0ZByG9dA/Th1f+sfGc571D95BnCPc90/pl2+2OdsQuOyoWEbeAMqfcTKo0v07i+E65Vw==}
- engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
- peerDependencies:
- size-limit: 12.1.0
-
- '@size-limit/file@12.1.0':
- resolution: {integrity: sha512-eGwDcIufnNnvJRzv3liDOn6MAOGgmOTUdpeGQ2KuRTlgIgO54AJH1ilvktlJc6PIjNfwpYY0dOGyap1QgM1swQ==}
- engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
- peerDependencies:
- size-limit: 12.1.0
-
- '@size-limit/preset-small-lib@12.1.0':
- resolution: {integrity: sha512-TVVQ/iuHbaGtHJrjur5s4XKYEyGk0nIwUAqhuzhKPbTyV9nYOH/laDelQ4vg3cGmm8sayRx998wxEdnwM/Yewg==}
- peerDependencies:
- size-limit: 12.1.0
-
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -1358,6 +1411,10 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
ansi-styles@5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
@@ -1397,6 +1454,9 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
+ before-after-hook@2.2.3:
+ resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
+
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
@@ -1416,10 +1476,6 @@ packages:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
- bytes-iec@3.1.1:
- resolution: {integrity: sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA==}
- engines: {node: '>= 0.8'}
-
cac@7.0.0:
resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
engines: {node: '>=20.19.0'}
@@ -1435,10 +1491,21 @@ packages:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
check-error@2.1.3:
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
engines: {node: '>= 16'}
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@@ -1487,6 +1554,9 @@ packages:
defu@6.1.6:
resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==}
+ deprecation@2.3.1:
+ resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
+
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@@ -1674,10 +1744,6 @@ packages:
engines: {node: '>=6'}
hasBin: true
- lilconfig@3.1.3:
- resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
- engines: {node: '>=14'}
-
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
@@ -1732,20 +1798,15 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
- nanoid@5.1.9:
- resolution: {integrity: sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==}
- engines: {node: ^18 || >=20}
- hasBin: true
-
- nanospinner@1.2.2:
- resolution: {integrity: sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==}
-
node-releases@2.0.37:
resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==}
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
open@10.2.0:
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
engines: {node: '>=18'}
@@ -1920,16 +1981,6 @@ packages:
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
engines: {node: '>=18'}
- size-limit@12.1.0:
- resolution: {integrity: sha512-VnDS2fycANrJFVPQwjaD+h+hkISY7EB3LsPsYWje4lBCjQwwsZLxjwwRwVJKHrcj2ZqyG+DdXykWm9mbZklZrw==}
- engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
- hasBin: true
- peerDependencies:
- jiti: ^2.0.0
- peerDependenciesMeta:
- jiti:
- optional: true
-
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -1990,10 +2041,6 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
- tinyglobby@0.2.16:
- resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
- engines: {node: '>=12.0.0'}
-
tinypool@2.1.0:
resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
engines: {node: ^20.0.0 || >=22.0.0}
@@ -2072,6 +2119,10 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ tunnel@0.0.6:
+ resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
+ engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
+
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@@ -2087,6 +2138,13 @@ packages:
resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==}
engines: {node: '>=20.18.1'}
+ universal-user-agent@6.0.1:
+ resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==}
+
+ unplugin@1.16.1:
+ resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==}
+ engines: {node: '>=14.0.0'}
+
unplugin@2.3.11:
resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==}
engines: {node: '>=18.12.0'}
@@ -2211,6 +2269,9 @@ packages:
engines: {node: '>=8'}
hasBin: true
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
ws@8.20.0:
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
engines: {node: '>=10.0.0'}
@@ -2237,8 +2298,37 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+ zod@3.25.76:
+ resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
+
snapshots:
+ '@actions/core@1.11.1':
+ dependencies:
+ '@actions/exec': 1.1.1
+ '@actions/http-client': 2.2.3
+
+ '@actions/exec@1.1.1':
+ dependencies:
+ '@actions/io': 1.1.3
+
+ '@actions/github@6.0.1':
+ dependencies:
+ '@actions/http-client': 2.2.3
+ '@octokit/core': 5.2.2
+ '@octokit/plugin-paginate-rest': 9.2.2(@octokit/core@5.2.2)
+ '@octokit/plugin-rest-endpoint-methods': 10.4.1(@octokit/core@5.2.2)
+ '@octokit/request': 8.4.1
+ '@octokit/request-error': 5.1.1
+ undici: 7.24.7
+
+ '@actions/http-client@2.2.3':
+ dependencies:
+ tunnel: 0.0.6
+ undici: 7.24.7
+
+ '@actions/io@1.1.3': {}
+
'@adobe/css-tools@4.4.4': {}
'@asamuzakjp/css-color@5.1.5':
@@ -2391,6 +2481,21 @@ snapshots:
dependencies:
css-tree: 3.2.1
+ '@codecov/bundler-plugin-core@1.9.1':
+ dependencies:
+ '@actions/core': 1.11.1
+ '@actions/github': 6.0.1
+ chalk: 4.1.2
+ semver: 7.7.4
+ unplugin: 1.16.1
+ zod: 3.25.76
+
+ '@codecov/rollup-plugin@1.9.1(rollup@4.60.1)':
+ dependencies:
+ '@codecov/bundler-plugin-core': 1.9.1
+ rollup: 4.60.1
+ unplugin: 1.16.1
+
'@csstools/color-helpers@6.0.2': {}
'@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
@@ -2629,6 +2734,64 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
+ '@octokit/auth-token@4.0.0': {}
+
+ '@octokit/core@5.2.2':
+ dependencies:
+ '@octokit/auth-token': 4.0.0
+ '@octokit/graphql': 7.1.1
+ '@octokit/request': 8.4.1
+ '@octokit/request-error': 5.1.1
+ '@octokit/types': 13.10.0
+ before-after-hook: 2.2.3
+ universal-user-agent: 6.0.1
+
+ '@octokit/endpoint@9.0.6':
+ dependencies:
+ '@octokit/types': 13.10.0
+ universal-user-agent: 6.0.1
+
+ '@octokit/graphql@7.1.1':
+ dependencies:
+ '@octokit/request': 8.4.1
+ '@octokit/types': 13.10.0
+ universal-user-agent: 6.0.1
+
+ '@octokit/openapi-types@20.0.0': {}
+
+ '@octokit/openapi-types@24.2.0': {}
+
+ '@octokit/plugin-paginate-rest@9.2.2(@octokit/core@5.2.2)':
+ dependencies:
+ '@octokit/core': 5.2.2
+ '@octokit/types': 12.6.0
+
+ '@octokit/plugin-rest-endpoint-methods@10.4.1(@octokit/core@5.2.2)':
+ dependencies:
+ '@octokit/core': 5.2.2
+ '@octokit/types': 12.6.0
+
+ '@octokit/request-error@5.1.1':
+ dependencies:
+ '@octokit/types': 13.10.0
+ deprecation: 2.3.1
+ once: 1.4.0
+
+ '@octokit/request@8.4.1':
+ dependencies:
+ '@octokit/endpoint': 9.0.6
+ '@octokit/request-error': 5.1.1
+ '@octokit/types': 13.10.0
+ universal-user-agent: 6.0.1
+
+ '@octokit/types@12.6.0':
+ dependencies:
+ '@octokit/openapi-types': 20.0.0
+
+ '@octokit/types@13.10.0':
+ dependencies:
+ '@octokit/openapi-types': 24.2.0
+
'@oxc-project/types@0.122.0': {}
'@oxfmt/binding-android-arm-eabi@0.43.0':
@@ -2883,22 +3046,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.60.1':
optional: true
- '@size-limit/esbuild@12.1.0(size-limit@12.1.0)':
- dependencies:
- esbuild: 0.28.0
- nanoid: 5.1.9
- size-limit: 12.1.0
-
- '@size-limit/file@12.1.0(size-limit@12.1.0)':
- dependencies:
- size-limit: 12.1.0
-
- '@size-limit/preset-small-lib@12.1.0(size-limit@12.1.0)':
- dependencies:
- '@size-limit/esbuild': 12.1.0(size-limit@12.1.0)
- '@size-limit/file': 12.1.0(size-limit@12.1.0)
- size-limit: 12.1.0
-
'@standard-schema/spec@1.1.0': {}
'@storybook/addon-docs@10.3.4(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@6.4.2(@types/node@20.19.35))':
@@ -3207,6 +3354,10 @@ snapshots:
ansi-regex@5.0.1: {}
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
ansi-styles@5.2.0: {}
ansis@4.2.0: {}
@@ -3239,6 +3390,8 @@ snapshots:
baseline-browser-mapping@2.10.14: {}
+ before-after-hook@2.2.3: {}
+
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
@@ -3261,8 +3414,6 @@ snapshots:
dependencies:
run-applescript: 7.1.0
- bytes-iec@3.1.1: {}
-
cac@7.0.0: {}
caniuse-lite@1.0.30001785: {}
@@ -3277,8 +3428,19 @@ snapshots:
chai@6.2.2: {}
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
check-error@2.1.3: {}
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
convert-source-map@2.0.0: {}
css-tree@3.2.1:
@@ -3316,6 +3478,8 @@ snapshots:
defu@6.1.6: {}
+ deprecation@2.3.1: {}
+
dequal@2.0.3: {}
doctrine@3.0.0:
@@ -3393,6 +3557,7 @@ snapshots:
'@esbuild/win32-arm64': 0.28.0
'@esbuild/win32-ia32': 0.28.0
'@esbuild/win32-x64': 0.28.0
+ optional: true
escalade@3.2.0: {}
@@ -3515,8 +3680,6 @@ snapshots:
json5@2.2.3: {}
- lilconfig@3.1.3: {}
-
loupe@3.2.1: {}
lru-cache@11.2.7: {}
@@ -3559,16 +3722,14 @@ snapshots:
nanoid@3.3.11: {}
- nanoid@5.1.9: {}
-
- nanospinner@1.2.2:
- dependencies:
- picocolors: 1.1.1
-
node-releases@2.0.37: {}
obug@2.1.1: {}
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
open@10.2.0:
dependencies:
default-browser: 5.5.0
@@ -3811,14 +3972,6 @@ snapshots:
mrmime: 2.0.1
totalist: 3.0.1
- size-limit@12.1.0:
- dependencies:
- bytes-iec: 3.1.1
- lilconfig: 3.1.3
- nanospinner: 1.2.2
- picocolors: 1.1.1
- tinyglobby: 0.2.16
-
source-map-js@1.2.1: {}
source-map@0.6.1: {}
@@ -3878,11 +4031,6 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
- tinyglobby@0.2.16:
- dependencies:
- fdir: 6.5.0(picomatch@4.0.4)
- picomatch: 4.0.4
-
tinypool@2.1.0: {}
tinyrainbow@2.0.0: {}
@@ -3946,6 +4094,8 @@ snapshots:
tslib@2.8.1: {}
+ tunnel@0.0.6: {}
+
typescript@5.9.3: {}
unconfig-core@7.5.0:
@@ -3957,6 +4107,13 @@ snapshots:
undici@7.24.7: {}
+ universal-user-agent@6.0.1: {}
+
+ unplugin@1.16.1:
+ dependencies:
+ acorn: 8.16.0
+ webpack-virtual-modules: 0.6.2
+
unplugin@2.3.11:
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -4042,6 +4199,8 @@ snapshots:
siginfo: 2.0.0
stackback: 0.0.2
+ wrappy@1.0.2: {}
+
ws@8.20.0: {}
wsl-utils@0.1.0:
@@ -4053,3 +4212,5 @@ snapshots:
xmlchars@2.2.0: {}
yallist@3.1.1: {}
+
+ zod@3.25.76: {}
diff --git a/scripts/react-compat-matrix.mjs b/scripts/react-compat-matrix.mjs
index 4626310..5911990 100644
--- a/scripts/react-compat-matrix.mjs
+++ b/scripts/react-compat-matrix.mjs
@@ -1,4 +1,4 @@
-import { mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
+import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -36,266 +36,13 @@ function getRequestedReactMatrix() {
];
}
-const reactMatrix = getRequestedReactMatrix();
-
-const smokeSource = `
-import { JSDOM } from 'jsdom';
-import React from 'react';
-import * as ReactDOM from 'react-dom';
-
-let ReactDOMClient = null;
-
-try {
- ReactDOMClient = await import('react-dom/client');
-} catch {
- ReactDOMClient = null;
-}
-
-const dom = new JSDOM('
', {
- pretendToBeVisual: true,
- url: 'http://localhost/',
-});
-
-for (const [key, value] of Object.entries({
- window: dom.window,
- document: dom.window.document,
- navigator: dom.window.navigator,
- HTMLElement: dom.window.HTMLElement,
- Element: dom.window.Element,
- Node: dom.window.Node,
- DOMRect: dom.window.DOMRect,
-})) {
- Object.defineProperty(globalThis, key, {
- configurable: true,
- value,
- writable: true,
- });
-}
-
-globalThis.requestAnimationFrame = callback => {
- callback(Date.now());
- return 1;
-};
-globalThis.cancelAnimationFrame = () => {};
-globalThis.ResizeObserver = class {
- observe() {}
- unobserve() {}
- disconnect() {}
-};
-globalThis.IntersectionObserver = class {
- observe() {}
- unobserve() {}
- disconnect() {}
-};
-
-const { AtomTrigger, useScrollPosition, useViewportSize } = await import('react-atom-trigger');
-
-async function createRenderer(container) {
- if (ReactDOMClient && typeof ReactDOMClient.createRoot === 'function') {
- const root = ReactDOMClient.createRoot(container);
- const render = element => {
- if (typeof ReactDOM.flushSync === 'function') {
- ReactDOM.flushSync(() => {
- root.render(element);
- });
- return;
- }
-
- root.render(element);
- };
-
- return {
- render,
- unmount: async () => {
- root.unmount();
- },
- };
- }
-
- return {
- render: element => {
- ReactDOM.render(element, container);
- },
- unmount: async () => {
- ReactDOM.unmountComponentAtNode(container);
- },
- };
-}
-
-function waitForTick() {
- return new Promise(resolve => setTimeout(resolve, 0));
-}
-
-async function waitForValue(read, label) {
- for (let attempt = 0; attempt < 20; attempt += 1) {
- const value = read();
- if (value) {
- return value;
- }
-
- await waitForTick();
- }
-
- throw new Error(\`Timed out while waiting for \${label}.\`);
-}
-
-async function runAtomTriggerSmoke() {
- const container = document.createElement('div');
- document.body.appendChild(container);
- const renderer = await createRenderer(container);
- renderer.render(React.createElement(AtomTrigger, { className: 'compat-sentinel' }));
- const sentinel = await waitForValue(() => container.querySelector('.compat-sentinel'), 'compat sentinel');
-
- if (!(sentinel instanceof HTMLElement)) {
- throw new Error('AtomTrigger sentinel smoke did not render as expected.');
- }
-
- await renderer.unmount();
- container.remove();
-}
-
-async function runChildModeSmoke() {
- const container = document.createElement('div');
- document.body.appendChild(container);
- const childRef = React.createRef();
-
- const ForwardedChild = React.forwardRef(function ForwardedChild(_, ref) {
- return React.createElement('section', { ref, className: 'compat-child' }, 'child');
- });
-
- const renderer = await createRenderer(container);
- renderer.render(
- React.createElement(
- AtomTrigger,
- {},
- React.createElement(ForwardedChild, {
- ref: childRef,
- }),
- ),
- );
- await waitForTick();
-
- const child = container.querySelector('.compat-child');
-
- if (!(child instanceof HTMLElement)) {
- throw new Error('Child mode smoke failed to render the forwarded child.');
- }
-
- if (!(childRef.current instanceof HTMLElement)) {
- throw new Error('Child mode smoke did not preserve the user ref.');
- }
-
- await renderer.unmount();
- container.remove();
-}
-
-async function runHooksSmoke() {
- const container = document.createElement('div');
- document.body.appendChild(container);
-
- function HooksHarness({ enabled }) {
- const position = useScrollPosition({ throttleMs: 0, enabled });
- const viewport = useViewportSize({ throttleMs: 0, enabled });
-
- return React.createElement(
- 'output',
- { id: 'hooks-output' },
- \`\${position.x},\${position.y}|\${viewport.width},\${viewport.height}\`,
- );
- }
-
- const renderer = await createRenderer(container);
- renderer.render(React.createElement(HooksHarness, { enabled: true }));
- await waitForTick();
-
- Object.defineProperty(window, 'scrollX', {
- configurable: true,
- value: 14,
- writable: true,
- });
- Object.defineProperty(window, 'scrollY', {
- configurable: true,
- value: 28,
- writable: true,
- });
- Object.defineProperty(window, 'innerWidth', {
- configurable: true,
- value: 1440,
- writable: true,
- });
- Object.defineProperty(window, 'innerHeight', {
- configurable: true,
- value: 900,
- writable: true,
- });
-
- window.dispatchEvent(new window.Event('scroll'));
- window.dispatchEvent(new window.Event('resize'));
- await waitForTick();
-
- const output = container.querySelector('#hooks-output');
- if (!(output instanceof HTMLElement)) {
- throw new Error('Hooks smoke did not render output.');
- }
-
- if (output.textContent !== '14,28|1440,900') {
- throw new Error(\`Hooks smoke failed, got "\${output.textContent}".\`);
- }
-
- renderer.render(React.createElement(HooksHarness, { enabled: false }));
- await waitForTick();
-
- Object.defineProperty(window, 'scrollX', {
- configurable: true,
- value: 18,
- writable: true,
- });
- Object.defineProperty(window, 'scrollY', {
- configurable: true,
- value: 32,
- writable: true,
- });
- Object.defineProperty(window, 'innerWidth', {
- configurable: true,
- value: 1600,
- writable: true,
- });
- Object.defineProperty(window, 'innerHeight', {
- configurable: true,
- value: 960,
- writable: true,
- });
-
- window.dispatchEvent(new window.Event('scroll'));
- window.dispatchEvent(new window.Event('resize'));
- await waitForTick();
-
- if (output.textContent !== '14,28|1440,900') {
- throw new Error(\`Hooks disabled smoke failed, got "\${output.textContent}".\`);
- }
-
- renderer.render(React.createElement(HooksHarness, { enabled: true }));
- await waitForTick();
-
- if (output.textContent !== '18,32|1600,960') {
- throw new Error(\`Hooks re-enable smoke failed, got "\${output.textContent}".\`);
- }
-
- await renderer.unmount();
- container.remove();
-}
-
-await runAtomTriggerSmoke();
-await runChildModeSmoke();
-await runHooksSmoke();
-`;
-
-function runCommand(command, args, cwd) {
+function runCommand(command, args, cwd, extraEnv = {}) {
const result = spawnSync(command, args, {
cwd,
stdio: 'inherit',
env: {
...process.env,
+ ...extraEnv,
npm_config_cache: npmCacheDir,
},
});
@@ -305,7 +52,10 @@ function runCommand(command, args, cwd) {
}
}
+const reactMatrix = getRequestedReactMatrix();
+const smokeSource = await readFile(path.join(dirname, 'react-compat-smoke.mjs'), 'utf8');
const npmCacheDir = await mkdtemp(path.join(os.tmpdir(), 'react-atom-trigger-npm-cache-'));
+
runCommand('pnpm', ['build'], repoRoot);
const packDir = await mkdtemp(path.join(os.tmpdir(), 'react-atom-trigger-pack-'));
runCommand('npm', ['pack', '--ignore-scripts', '--pack-destination', packDir], repoRoot);
@@ -342,7 +92,9 @@ try {
await writeFile(path.join(tempDir, 'smoke.mjs'), smokeSource);
runCommand('npm', ['install', '--no-package-lock'], tempDir);
- runCommand('node', ['smoke.mjs'], tempDir);
+ runCommand('node', ['smoke.mjs'], tempDir, {
+ REACT_ATOM_TRIGGER_IMPORT: 'react-atom-trigger',
+ });
} finally {
await rm(tempDir, { recursive: true, force: true });
}
diff --git a/scripts/react-compat-smoke.mjs b/scripts/react-compat-smoke.mjs
index fed545e..8a14922 100644
--- a/scripts/react-compat-smoke.mjs
+++ b/scripts/react-compat-smoke.mjs
@@ -47,7 +47,8 @@ globalThis.IntersectionObserver = class {
disconnect() {}
};
-const { AtomTrigger, useScrollPosition, useViewportSize } = await import('../lib/index.js');
+const packageImport = process.env.REACT_ATOM_TRIGGER_IMPORT ?? '../lib/index.js';
+const { AtomTrigger, useScrollPosition, useViewportSize } = await import(packageImport);
async function createRenderer(container) {
if (ReactDOMClient && typeof ReactDOMClient.createRoot === 'function') {
diff --git a/src/AtomTrigger.childMode.helpers.test.ts b/src/AtomTrigger.childMode.helpers.test.ts
index da80057..4158759 100644
--- a/src/AtomTrigger.childMode.helpers.test.ts
+++ b/src/AtomTrigger.childMode.helpers.test.ts
@@ -8,13 +8,7 @@ import {
useObservedChildNode,
type ChildElementWithOptionalRef,
} from './AtomTrigger.childMode';
-import {
- fragmentChildWarning,
- getWarningMessage,
- invalidChildCountWarning,
- invalidChildElementWarning,
- unsupportedChildRefWarning,
-} from './AtomTrigger.warnings';
+import { warningMessages } from './AtomTrigger.warnings';
const initialNodeEnv = process.env.NODE_ENV;
@@ -90,16 +84,16 @@ describe('AtomTrigger child mode helpers', () => {
});
it('warns when more than one top-level child is passed', () => {
- expect(getInvalidChildWarning(true, 2, childElement)).toBe(invalidChildCountWarning);
+ expect(getInvalidChildWarning(true, 2, childElement)).toBe(warningMessages.invalidChildCount);
});
it('warns when the child is not a React element', () => {
- expect(getInvalidChildWarning(true, 1, null)).toBe(invalidChildElementWarning);
+ expect(getInvalidChildWarning(true, 1, null)).toBe(warningMessages.invalidChildElement);
});
it('warns when the child is a fragment', () => {
expect(getInvalidChildWarning(true, 1, React.createElement(React.Fragment))).toBe(
- fragmentChildWarning,
+ warningMessages.fragmentChild,
);
});
@@ -202,7 +196,7 @@ describe('AtomTrigger child mode helpers', () => {
vi.advanceTimersByTime(16);
});
- expect(warn).not.toHaveBeenCalledWith(getWarningMessage(unsupportedChildRefWarning));
+ expect(warn).not.toHaveBeenCalledWith(warningMessages.unsupportedChildRef);
});
it('keeps the observed child node stable when the same DOM ref is attached again', () => {
@@ -258,7 +252,7 @@ describe('AtomTrigger child mode helpers', () => {
vi.advanceTimersByTime(16);
});
- expect(warn).not.toHaveBeenCalledWith(getWarningMessage(unsupportedChildRefWarning));
+ expect(warn).not.toHaveBeenCalledWith(warningMessages.unsupportedChildRef);
});
});
});
diff --git a/src/AtomTrigger.childMode.test.tsx b/src/AtomTrigger.childMode.test.tsx
index 8ab04a5..6f07132 100644
--- a/src/AtomTrigger.childMode.test.tsx
+++ b/src/AtomTrigger.childMode.test.tsx
@@ -10,11 +10,7 @@ import {
setRect,
setupChildRootHarness,
} from './AtomTrigger.testUtils';
-import {
- getWarningMessage,
- nonDomChildRefWarning,
- unsupportedChildRefWarning,
-} from './AtomTrigger.warnings';
+import { warningMessages } from './AtomTrigger.warnings';
beforeEach(() => {
prepareDomTestRun();
@@ -104,7 +100,7 @@ describe('AtomTrigger child mode', () => {
);
expect(view.getByTestId('imperative-handle-child')).toBeTruthy();
- expect(warn).toHaveBeenCalledWith(getWarningMessage(nonDomChildRefWarning));
+ expect(warn).toHaveBeenCalledWith(warningMessages.nonDomChildRef);
expect(error).not.toHaveBeenCalled();
});
@@ -171,9 +167,7 @@ describe('AtomTrigger child mode', () => {
});
expect(
- warn.mock.calls.some(
- ([message]) => message === getWarningMessage(unsupportedChildRefWarning),
- ),
+ warn.mock.calls.some(([message]) => message === warningMessages.unsupportedChildRef),
).toBe(false);
});
diff --git a/src/AtomTrigger.childMode.ts b/src/AtomTrigger.childMode.ts
index 01a3abc..a73fb1d 100644
--- a/src/AtomTrigger.childMode.ts
+++ b/src/AtomTrigger.childMode.ts
@@ -1,14 +1,5 @@
import React from 'react';
-import {
- fragmentChildWarning,
- getWarningMessage,
- invalidChildCountWarning,
- invalidChildElementWarning,
- nonDomChildRefWarning,
- unsupportedChildRefWarning,
- type AtomTriggerWarning,
- warnOnce,
-} from './AtomTrigger.warnings';
+import { warningMessages, warnOnce } from './AtomTrigger.warnings';
import { isDomElementLike } from './AtomTrigger.runtime';
const missingDomRefWarningDelayMs = 16;
@@ -70,21 +61,21 @@ export function getInvalidChildWarning(
usesChildObservation: boolean,
childCount: number,
singleChildElement: React.ReactElement | null,
-): AtomTriggerWarning | null {
+): string | null {
if (!usesChildObservation) {
return null;
}
if (childCount !== 1) {
- return invalidChildCountWarning;
+ return warningMessages.invalidChildCount;
}
if (!singleChildElement) {
- return invalidChildElementWarning;
+ return warningMessages.invalidChildElement;
}
if (singleChildElement.type === React.Fragment) {
- return fragmentChildWarning;
+ return warningMessages.fragmentChild;
}
return null;
@@ -98,7 +89,7 @@ export function useObservedChildNode({
}: {
originalChildRef: React.Ref | undefined;
hasObservedChild: boolean;
- invalidChildWarning: AtomTriggerWarning | null;
+ invalidChildWarning: string | null;
shouldWarnAboutMissingDomRef: boolean;
}): ObservedChildBinding {
const [childNode, setChildNode] = React.useState(null);
@@ -126,7 +117,7 @@ export function useObservedChildNode({
clearObservedChildNode();
if (process.env.NODE_ENV === 'development') {
- warnOnce(getWarningMessage(nonDomChildRefWarning));
+ warnOnce(warningMessages.nonDomChildRef);
}
},
[clearObservedChildNode, originalChildRef],
@@ -146,7 +137,7 @@ export function useObservedChildNode({
}
if (process.env.NODE_ENV === 'development') {
- warnOnce(getWarningMessage(unsupportedChildRefWarning));
+ warnOnce(warningMessages.unsupportedChildRef);
}
}, missingDomRefWarningDelayMs);
diff --git a/src/AtomTrigger.observation.test.ts b/src/AtomTrigger.observation.test.ts
index 0105ca9..4a50df4 100644
--- a/src/AtomTrigger.observation.test.ts
+++ b/src/AtomTrigger.observation.test.ts
@@ -2,9 +2,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import * as scheduler from './AtomTrigger.scheduler';
import * as sampling from './AtomTrigger.sampling';
import {
- createObservationController,
- disposeObservationController,
- reconcileObservationBinding,
+ cleanupObservationState,
+ createObservationState,
+ syncObservationSubscription,
updateObservationCallbacks,
} from './AtomTrigger.observation';
@@ -12,7 +12,7 @@ function createNode(): Element {
return document.createElement('div');
}
-describe('AtomTrigger observation controller', () => {
+describe('AtomTrigger observation state', () => {
afterEach(() => {
vi.restoreAllMocks();
});
@@ -21,7 +21,7 @@ describe('AtomTrigger observation controller', () => {
const node = createNode();
const firstOnEnter = vi.fn();
const secondOnEnter = vi.fn();
- const controller = createObservationController(
+ const observation = createObservationState(
{
node,
rootMargin: '0px',
@@ -33,15 +33,15 @@ describe('AtomTrigger observation controller', () => {
{ onEnter: firstOnEnter },
);
- updateObservationCallbacks(controller, { onEnter: secondOnEnter });
+ updateObservationCallbacks(observation, { onEnter: secondOnEnter });
- expect(controller.registration.node).toBe(node);
- expect(controller.registration.onEnter).toBe(secondOnEnter);
+ expect(observation.registration.node).toBe(node);
+ expect(observation.registration.onEnter).toBe(secondOnEnter);
});
- it('clears binding state when no node is available', () => {
- const dispose = vi.fn();
- const controller = createObservationController(
+ it('clears subscription state when no node is available', () => {
+ const unsubscribe = vi.fn();
+ const observation = createObservationState(
{
node: createNode(),
rootMargin: '0px',
@@ -53,8 +53,8 @@ describe('AtomTrigger observation controller', () => {
{},
);
- controller.dispose = dispose;
- controller.binding = {
+ observation.unsubscribe = unsubscribe;
+ observation.subscription = {
node: createNode(),
target: window,
rootMargin: '0px',
@@ -64,7 +64,7 @@ describe('AtomTrigger observation controller', () => {
fireOnInitialVisible: false,
};
- reconcileObservationBinding(controller, {
+ syncObservationSubscription(observation, {
disabled: false,
node: null,
target: window,
@@ -75,14 +75,14 @@ describe('AtomTrigger observation controller', () => {
fireOnInitialVisible: false,
});
- expect(dispose).toHaveBeenCalledTimes(1);
- expect(controller.binding).toBeNull();
- expect(controller.dispose).toBeNull();
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
+ expect(observation.subscription).toBeNull();
+ expect(observation.unsubscribe).toBeNull();
});
it('resets and stays unsubscribed when disabled or missing a target', () => {
const resetSpy = vi.spyOn(sampling, 'resetObservationState');
- const controller = createObservationController(
+ const observation = createObservationState(
{
node: createNode(),
rootMargin: '0px',
@@ -94,9 +94,9 @@ describe('AtomTrigger observation controller', () => {
{},
);
- reconcileObservationBinding(controller, {
+ syncObservationSubscription(observation, {
disabled: true,
- node: controller.registration.node,
+ node: observation.registration.node,
target: null,
rootMargin: '10px',
threshold: 1,
@@ -105,15 +105,15 @@ describe('AtomTrigger observation controller', () => {
fireOnInitialVisible: true,
});
- expect(resetSpy).toHaveBeenCalledWith(controller.registration);
- expect(controller.binding).toBeNull();
- expect(controller.dispose).toBeNull();
- expect(controller.registration.node).toBe(controller.registration.node);
+ expect(resetSpy).toHaveBeenCalledWith(observation.registration);
+ expect(observation.subscription).toBeNull();
+ expect(observation.unsubscribe).toBeNull();
+ expect(observation.registration.node).toBe(observation.registration.node);
});
- it('avoids resubscribing when the binding snapshot is unchanged', () => {
+ it('avoids resubscribing when the subscription snapshot is unchanged', () => {
const registerSpy = vi.spyOn(scheduler, 'registerSentinel').mockReturnValue(vi.fn());
- const controller = createObservationController(
+ const observation = createObservationState(
{
node: createNode(),
rootMargin: '0px',
@@ -127,7 +127,7 @@ describe('AtomTrigger observation controller', () => {
const input = {
disabled: false,
- node: controller.registration.node,
+ node: observation.registration.node,
target: window,
rootMargin: '0px',
threshold: 0,
@@ -136,15 +136,15 @@ describe('AtomTrigger observation controller', () => {
fireOnInitialVisible: false,
} as const;
- reconcileObservationBinding(controller, input);
- reconcileObservationBinding(controller, input);
+ syncObservationSubscription(observation, input);
+ syncObservationSubscription(observation, input);
expect(registerSpy).toHaveBeenCalledTimes(1);
});
- it('cleans up an active subscription when the controller is disposed', () => {
- const dispose = vi.fn();
- const controller = createObservationController(
+ it('cleans up an active subscription when the observation state is cleaned up', () => {
+ const unsubscribe = vi.fn();
+ const observation = createObservationState(
{
node: createNode(),
rootMargin: '0px',
@@ -156,11 +156,11 @@ describe('AtomTrigger observation controller', () => {
{},
);
- controller.dispose = dispose;
- disposeObservationController(controller);
+ observation.unsubscribe = unsubscribe;
+ cleanupObservationState(observation);
- expect(dispose).toHaveBeenCalledTimes(1);
- expect(controller.binding).toBeNull();
- expect(controller.dispose).toBeNull();
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
+ expect(observation.subscription).toBeNull();
+ expect(observation.unsubscribe).toBeNull();
});
});
diff --git a/src/AtomTrigger.observation.ts b/src/AtomTrigger.observation.ts
index e2a777e..4a7204d 100644
--- a/src/AtomTrigger.observation.ts
+++ b/src/AtomTrigger.observation.ts
@@ -18,14 +18,14 @@ export type ObservationCallbacks = {
onEvent?: (event: AtomTriggerEvent) => void;
};
-export type ObservationBinding = ObservationConfig & {
+export type SubscriptionSnapshot = ObservationConfig & {
target: SchedulerTarget;
};
-export type ObservationController = {
+export type ObservationState = {
registration: SentinelRegistration;
- binding: ObservationBinding | null;
- dispose: (() => void) | null;
+ subscription: SubscriptionSnapshot | null;
+ unsubscribe: (() => void) | null;
};
function createRegistration(
@@ -44,34 +44,34 @@ function createRegistration(
};
}
-function clearObservationBinding(controller: ObservationController): void {
- controller.dispose?.();
- controller.dispose = null;
- controller.binding = null;
+function clearObservationSubscription(observation: ObservationState): void {
+ observation.unsubscribe?.();
+ observation.unsubscribe = null;
+ observation.subscription = null;
}
-export function createObservationController(
+export function createObservationState(
config: ObservationConfig,
callbacks: ObservationCallbacks,
-): ObservationController {
+): ObservationState {
return {
registration: createRegistration(config, callbacks),
- binding: null,
- dispose: null,
+ subscription: null,
+ unsubscribe: null,
};
}
export function updateObservationCallbacks(
- controller: ObservationController,
+ observation: ObservationState,
callbacks: ObservationCallbacks,
): void {
- controller.registration.onEnter = callbacks.onEnter;
- controller.registration.onLeave = callbacks.onLeave;
- controller.registration.onEvent = callbacks.onEvent;
+ observation.registration.onEnter = callbacks.onEnter;
+ observation.registration.onLeave = callbacks.onLeave;
+ observation.registration.onEvent = callbacks.onEvent;
}
-export function reconcileObservationBinding(
- controller: ObservationController,
+export function syncObservationSubscription(
+ observation: ObservationState,
input: {
disabled: boolean;
node: Element | null;
@@ -83,11 +83,11 @@ export function reconcileObservationBinding(
fireOnInitialVisible: boolean;
},
): void {
- const registration = controller.registration;
+ const registration = observation.registration;
if (!input.node) {
resetObservationState(registration);
- clearObservationBinding(controller);
+ clearObservationSubscription(observation);
return;
}
@@ -101,39 +101,39 @@ export function reconcileObservationBinding(
};
if (input.disabled || !input.target) {
- clearObservationBinding(controller);
+ clearObservationSubscription(observation);
Object.assign(registration, nextConfig);
resetObservationState(registration);
return;
}
- const nextBinding: ObservationBinding = {
+ const nextSubscription: SubscriptionSnapshot = {
...nextConfig,
target: input.target,
};
- const bindingUnchanged =
- controller.binding !== null &&
- controller.binding.node === nextBinding.node &&
- controller.binding.target === nextBinding.target &&
- controller.binding.rootMargin === nextBinding.rootMargin &&
- controller.binding.threshold === nextBinding.threshold &&
- controller.binding.once === nextBinding.once &&
- controller.binding.oncePerDirection === nextBinding.oncePerDirection &&
- controller.binding.fireOnInitialVisible === nextBinding.fireOnInitialVisible;
-
- if (bindingUnchanged) {
+ const subscriptionUnchanged =
+ observation.subscription !== null &&
+ observation.subscription.node === nextSubscription.node &&
+ observation.subscription.target === nextSubscription.target &&
+ observation.subscription.rootMargin === nextSubscription.rootMargin &&
+ observation.subscription.threshold === nextSubscription.threshold &&
+ observation.subscription.once === nextSubscription.once &&
+ observation.subscription.oncePerDirection === nextSubscription.oncePerDirection &&
+ observation.subscription.fireOnInitialVisible === nextSubscription.fireOnInitialVisible;
+
+ if (subscriptionUnchanged) {
Object.assign(registration, nextConfig);
return;
}
resetObservationState(registration);
- clearObservationBinding(controller);
+ clearObservationSubscription(observation);
Object.assign(registration, nextConfig);
- controller.dispose = registerSentinel(input.target, registration);
- controller.binding = nextBinding;
+ observation.unsubscribe = registerSentinel(input.target, registration);
+ observation.subscription = nextSubscription;
}
-export function disposeObservationController(controller: ObservationController): void {
- clearObservationBinding(controller);
+export function cleanupObservationState(observation: ObservationState): void {
+ clearObservationSubscription(observation);
}
diff --git a/src/AtomTrigger.root.ts b/src/AtomTrigger.root.ts
index a8332e7..c39528d 100644
--- a/src/AtomTrigger.root.ts
+++ b/src/AtomTrigger.root.ts
@@ -1,11 +1,6 @@
import React from 'react';
import { isDomElementLike } from './AtomTrigger.runtime';
-import {
- getWarningMessage,
- invalidRootRefWarning,
- invalidRootWarning,
- warnOnce,
-} from './AtomTrigger.warnings';
+import { warningMessages, warnOnce } from './AtomTrigger.warnings';
export type SchedulerTarget = Window | Element;
@@ -17,7 +12,8 @@ export type SchedulerTargetSource =
function resolveExplicitRootTarget(
source: Extract,
): Element | null {
- const warningMessage = source.kind === 'rootRef' ? invalidRootRefWarning : invalidRootWarning;
+ const warningMessage =
+ source.kind === 'rootRef' ? warningMessages.invalidRootRef : warningMessages.invalidRoot;
const { target } = source;
if (target === null || target === undefined) {
@@ -29,7 +25,7 @@ function resolveExplicitRootTarget(
}
if (process.env.NODE_ENV === 'development') {
- warnOnce(getWarningMessage(warningMessage));
+ warnOnce(warningMessage);
}
return null;
}
diff --git a/src/AtomTrigger.sampling.ts b/src/AtomTrigger.sampling.ts
index fc17594..626a9fb 100644
--- a/src/AtomTrigger.sampling.ts
+++ b/src/AtomTrigger.sampling.ts
@@ -130,6 +130,6 @@ export function sampleRegistration(
}
if (isRegistrationComplete(registration)) {
- registration.dispose?.();
+ registration.unsubscribe?.();
}
}
diff --git a/src/AtomTrigger.scheduler.test.ts b/src/AtomTrigger.scheduler.test.ts
index c3a660a..e3c822e 100644
--- a/src/AtomTrigger.scheduler.test.ts
+++ b/src/AtomTrigger.scheduler.test.ts
@@ -4,11 +4,7 @@ import { registerSentinel, type SentinelRegistration } from './AtomTrigger.sched
import { resolveSchedulerTarget } from './AtomTrigger.root';
import { resetObservationState } from './AtomTrigger.sampling';
import { finishDomTestRun, prepareDomTestRun, setNodeEnv, setRect } from './AtomTrigger.testUtils';
-import {
- getWarningMessage,
- invalidRootRefWarning,
- invalidRootWarning,
-} from './AtomTrigger.warnings';
+import { warningMessages } from './AtomTrigger.warnings';
function createRegistration(
node: Element,
@@ -131,8 +127,8 @@ describe('AtomTrigger scheduler helpers', () => {
expect(resolveSchedulerTarget({ kind: 'root', target: pseudoRoot })).toBeNull();
expect(resolveSchedulerTarget({ kind: 'rootRef', target: pseudoRoot })).toBeNull();
- expect(warn).toHaveBeenCalledWith(getWarningMessage(invalidRootWarning));
- expect(warn).toHaveBeenCalledWith(getWarningMessage(invalidRootRefWarning));
+ expect(warn).toHaveBeenCalledWith(warningMessages.invalidRoot);
+ expect(warn).toHaveBeenCalledWith(warningMessages.invalidRootRef);
});
it('keeps invalid explicit root warnings out of non-development runtimes', () => {
@@ -195,7 +191,7 @@ describe('AtomTrigger scheduler helpers', () => {
expect(resizeObservers[0].disconnect).toHaveBeenCalledTimes(1);
expect(intersectionObservers[0].unobserve).toHaveBeenCalledWith(node);
expect(intersectionObservers[0].disconnect).toHaveBeenCalledTimes(1);
- expect(registration.dispose).toBeUndefined();
+ expect(registration.unsubscribe).toBeUndefined();
});
it('uses viewport observers without trying to observe the window target directly', () => {
diff --git a/src/AtomTrigger.scheduler.ts b/src/AtomTrigger.scheduler.ts
index 1ca6568..8ea62ff 100644
--- a/src/AtomTrigger.scheduler.ts
+++ b/src/AtomTrigger.scheduler.ts
@@ -12,7 +12,7 @@ export type SentinelRegistration = {
once: boolean;
oncePerDirection: boolean;
fireOnInitialVisible: boolean;
- dispose?: () => void;
+ unsubscribe?: () => void;
onEnter?: (event: AtomTriggerEvent) => void;
onLeave?: (event: AtomTriggerEvent) => void;
onEvent?: (event: AtomTriggerEvent) => void;
@@ -190,7 +190,7 @@ export function registerSentinel(
scheduler.registrations.delete(registration);
scheduler.resizeObserver?.unobserve(observedNode);
scheduler.intersectionObserver?.unobserve(observedNode);
- registration.dispose = undefined;
+ registration.unsubscribe = undefined;
if (scheduler.registrations.size === 0) {
scheduler.cleanup();
@@ -198,7 +198,7 @@ export function registerSentinel(
}
};
- registration.dispose = dispose;
+ registration.unsubscribe = dispose;
return dispose;
}
diff --git a/src/AtomTrigger.tsx b/src/AtomTrigger.tsx
index 47e13bc..dfb7dcc 100644
--- a/src/AtomTrigger.tsx
+++ b/src/AtomTrigger.tsx
@@ -8,23 +8,18 @@ import {
} from './AtomTrigger.childMode';
import { normalizeRootMargin, normalizeThreshold } from './AtomTrigger.geometry';
import {
- createObservationController,
- disposeObservationController,
- reconcileObservationBinding,
+ cleanupObservationState,
+ createObservationState,
+ syncObservationSubscription,
updateObservationCallbacks,
- type ObservationController,
+ type ObservationState,
} from './AtomTrigger.observation';
import {
resolveSchedulerTarget,
useTrackedRootRefTarget,
type SchedulerTargetSource,
} from './AtomTrigger.root';
-import {
- childModeClassNameWarning,
- conflictingOnceModesWarning,
- getWarningMessage,
- warnOnce,
-} from './AtomTrigger.warnings';
+import { warningMessages, warnOnce } from './AtomTrigger.warnings';
const defaultSentinelStyle = { display: 'table' } satisfies React.CSSProperties;
@@ -44,7 +39,7 @@ const AtomTrigger: React.FC = ({
className,
}) => {
const sentinelRef = React.useRef(null);
- const controllerRef = React.useRef(null);
+ const observationRef = React.useRef(null);
const trackedRootRefTarget = useTrackedRootRefTarget(rootRef);
const normalizedRootMargin = normalizeRootMargin(rootMargin);
@@ -74,29 +69,29 @@ const AtomTrigger: React.FC = ({
React.useEffect(() => {
if (process.env.NODE_ENV === 'development' && hasObservedChild && className) {
- warnOnce(getWarningMessage(childModeClassNameWarning));
+ warnOnce(warningMessages.childModeClassName);
}
}, [className, hasObservedChild]);
React.useEffect(() => {
if (process.env.NODE_ENV === 'development' && invalidChildWarning) {
- warnOnce(getWarningMessage(invalidChildWarning));
+ warnOnce(invalidChildWarning);
}
}, [invalidChildWarning]);
React.useEffect(() => {
if (process.env.NODE_ENV === 'development' && once && oncePerDirection) {
- warnOnce(getWarningMessage(conflictingOnceModesWarning));
+ warnOnce(warningMessages.conflictingOnceModes);
}
}, [once, oncePerDirection]);
React.useEffect(() => {
- const controller = controllerRef.current;
- if (!controller) {
+ const observation = observationRef.current;
+ if (!observation) {
return;
}
- updateObservationCallbacks(controller, { onEnter, onLeave, onEvent });
+ updateObservationCallbacks(observation, { onEnter, onLeave, onEvent });
}, [onEnter, onLeave, onEvent]);
React.useEffect(() => {
@@ -114,8 +109,8 @@ const AtomTrigger: React.FC = ({
const resolvedRoot = resolveSchedulerTarget(targetSource);
if (!node) {
- if (controllerRef.current) {
- reconcileObservationBinding(controllerRef.current, {
+ if (observationRef.current) {
+ syncObservationSubscription(observationRef.current, {
disabled: false,
node: null,
target: resolvedRoot,
@@ -129,8 +124,8 @@ const AtomTrigger: React.FC = ({
return;
}
- if (!controllerRef.current) {
- controllerRef.current = createObservationController(
+ if (!observationRef.current) {
+ observationRef.current = createObservationState(
{
node,
rootMargin: normalizedRootMargin,
@@ -143,7 +138,7 @@ const AtomTrigger: React.FC = ({
);
}
- reconcileObservationBinding(controllerRef.current, {
+ syncObservationSubscription(observationRef.current, {
disabled,
node,
target: resolvedRoot,
@@ -169,12 +164,12 @@ const AtomTrigger: React.FC = ({
React.useEffect(
() => () => {
- if (!controllerRef.current) {
+ if (!observationRef.current) {
return;
}
- disposeObservationController(controllerRef.current);
- controllerRef.current = null;
+ cleanupObservationState(observationRef.current);
+ observationRef.current = null;
},
[],
);
diff --git a/src/AtomTrigger.warnings.ts b/src/AtomTrigger.warnings.ts
index 72430e5..8bbb9e9 100644
--- a/src/AtomTrigger.warnings.ts
+++ b/src/AtomTrigger.warnings.ts
@@ -1,55 +1,25 @@
const devWarnings = new Set();
-export type AtomTriggerWarning =
- | 'invalidChildCount'
- | 'invalidChildElement'
- | 'unsupportedChildRef'
- | 'fragmentChild'
- | 'nonDomChildRef'
- | 'childModeClassName'
- | 'conflictingOnceModes'
- | 'invalidRoot'
- | 'invalidRootRef';
-export const invalidChildCountWarning = 'invalidChildCount' satisfies AtomTriggerWarning;
-
-export const invalidChildElementWarning = 'invalidChildElement' satisfies AtomTriggerWarning;
-
-export const unsupportedChildRefWarning = 'unsupportedChildRef' satisfies AtomTriggerWarning;
-
-export const fragmentChildWarning = 'fragmentChild' satisfies AtomTriggerWarning;
-
-export const nonDomChildRefWarning = 'nonDomChildRef' satisfies AtomTriggerWarning;
-
-export const childModeClassNameWarning = 'childModeClassName' satisfies AtomTriggerWarning;
-
-export const conflictingOnceModesWarning = 'conflictingOnceModes' satisfies AtomTriggerWarning;
-
-export const invalidRootWarning = 'invalidRoot' satisfies AtomTriggerWarning;
-
-export const invalidRootRefWarning = 'invalidRootRef' satisfies AtomTriggerWarning;
-
-export function getWarningMessage(warning: AtomTriggerWarning): string {
- switch (warning) {
- case 'invalidChildCount':
- return '[react-atom-trigger] Child mode expects exactly one top-level React element. Observation is disabled for this render.';
- case 'invalidChildElement':
- return '[react-atom-trigger] Child mode expects a React element child. Observation is disabled for this render.';
- case 'unsupportedChildRef':
- return '[react-atom-trigger] Child mode expects a DOM element or a component that forwards its ref to a DOM element. Observation is disabled for this render.';
- case 'fragmentChild':
- return '[react-atom-trigger] Child mode does not support React.Fragment. Wrap the content in a single DOM element. Observation is disabled for this render.';
- case 'nonDomChildRef':
- return '[react-atom-trigger] Child mode requires the child ref to resolve to a DOM element. Observation is disabled for this render.';
- case 'childModeClassName':
- return '[react-atom-trigger] `className` only applies to the internal sentinel. In child mode, style the child element directly.';
- case 'conflictingOnceModes':
- return '[react-atom-trigger] `once` and `oncePerDirection` were both provided. `once` takes precedence.';
- case 'invalidRoot':
- return '[react-atom-trigger] `root` must be a real DOM element when provided. Observation is paused until it is.';
- case 'invalidRootRef':
- return '[react-atom-trigger] `rootRef.current` must resolve to a real DOM element. Observation is paused until it does.';
- }
-}
+export const warningMessages = {
+ invalidChildCount:
+ '[react-atom-trigger] Child mode expects exactly one top-level React element. Observation is disabled for this render.',
+ invalidChildElement:
+ '[react-atom-trigger] Child mode expects a React element child. Observation is disabled for this render.',
+ unsupportedChildRef:
+ '[react-atom-trigger] Child mode expects a DOM element or a component that forwards its ref to a DOM element. Observation is disabled for this render.',
+ fragmentChild:
+ '[react-atom-trigger] Child mode does not support React.Fragment. Wrap the content in a single DOM element. Observation is disabled for this render.',
+ nonDomChildRef:
+ '[react-atom-trigger] Child mode requires the child ref to resolve to a DOM element. Observation is disabled for this render.',
+ childModeClassName:
+ '[react-atom-trigger] `className` only applies to the internal sentinel. In child mode, style the child element directly.',
+ conflictingOnceModes:
+ '[react-atom-trigger] `once` and `oncePerDirection` were both provided. `once` takes precedence.',
+ invalidRoot:
+ '[react-atom-trigger] `root` must be a real DOM element when provided. Observation is paused until it is.',
+ invalidRootRef:
+ '[react-atom-trigger] `rootRef.current` must resolve to a real DOM element. Observation is paused until it does.',
+} as const;
function getKnownNodeEnv(): 'development' | 'production' | null {
if (typeof process === 'undefined' || !process.env) {
diff --git a/src/stories/components/InteractionHarness/ChildModeInteractionHarness.tsx b/src/stories/components/InteractionHarness/ChildModeInteractionHarness.tsx
index 38f2f5a..8af2774 100644
--- a/src/stories/components/InteractionHarness/ChildModeInteractionHarness.tsx
+++ b/src/stories/components/InteractionHarness/ChildModeInteractionHarness.tsx
@@ -1,7 +1,15 @@
import React from 'react';
import { AtomTrigger } from '../../../index';
import type { AtomTriggerEvent } from '../../../index';
-import { addHarnessEvent, CounterPanel, type ChildModeInteractionHarnessProps } from './shared';
+import {
+ addHarnessEvent,
+ CounterPanel,
+ dispatchElementScroll,
+ markHarnessReady,
+ mockElementRect,
+ runFrameSequence,
+ type ChildModeInteractionHarnessProps,
+} from './shared';
export function ChildModeInteractionHarness({
threshold = 0,
@@ -25,20 +33,12 @@ export function ChildModeInteractionHarness({
return;
}
- Object.defineProperty(root, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(0, 0, 200, 200),
- });
- Object.defineProperty(child, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(20, 260 - scrollTopRef.current, 160, 100),
- });
+ mockElementRect(root, () => new DOMRect(0, 0, 200, 200));
+ mockElementRect(child, () => new DOMRect(20, 260 - scrollTopRef.current, 160, 100));
scrollTopRef.current = 0;
setCurrentScrollTop(0);
- const readyId = window.requestAnimationFrame(() => {
- setHarnessReady(true);
- });
+ const readyId = markHarnessReady(setHarnessReady);
return () => {
window.cancelAnimationFrame(readyId);
@@ -53,11 +53,7 @@ export function ChildModeInteractionHarness({
scrollTopRef.current = nextTop;
setCurrentScrollTop(nextTop);
- root.dispatchEvent(
- new root.ownerDocument.defaultView!.Event('scroll', {
- bubbles: true,
- }),
- );
+ dispatchElementScroll(root);
}, []);
const triggerBasicEnter = React.useCallback(() => {
@@ -78,19 +74,15 @@ export function ChildModeInteractionHarness({
const runSequence = React.useCallback(() => {
scrollVertical(0);
-
- window.requestAnimationFrame(() => {
- scrollVertical(threshold > 0 ? 134 : 120);
- window.requestAnimationFrame(() => {
+ runFrameSequence([
+ () => scrollVertical(threshold > 0 ? 134 : 120),
+ () => {
if (threshold > 0) {
scrollVertical(135);
}
-
- window.requestAnimationFrame(() => {
- scrollVertical(360);
- });
- });
- });
+ },
+ () => scrollVertical(360),
+ ]);
}, [scrollVertical, threshold]);
const resetHarness = React.useCallback(() => {
diff --git a/src/stories/components/InteractionHarness/FixedHeaderViewportHarness.tsx b/src/stories/components/InteractionHarness/FixedHeaderViewportHarness.tsx
index 095379c..3228e64 100644
--- a/src/stories/components/InteractionHarness/FixedHeaderViewportHarness.tsx
+++ b/src/stories/components/InteractionHarness/FixedHeaderViewportHarness.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { AtomTrigger } from '../../../index';
import type { AtomTriggerEvent, AtomTriggerProps } from '../../../index';
-import { addHarnessEvent } from './shared';
+import { addHarnessEvent, markHarnessReady, mockElementRect } from './shared';
type FixedHeaderViewportHarnessProps = Pick<
AtomTriggerProps,
@@ -38,16 +38,11 @@ export function FixedHeaderViewportHarness({
configurable: true,
value: 200,
});
- Object.defineProperty(sentinel, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(40, 260 - scrollTopRef.current, 120, 2),
- });
+ mockElementRect(sentinel, () => new DOMRect(40, 260 - scrollTopRef.current, 120, 2));
scrollTopRef.current = 0;
setCurrentScrollTop(0);
- const readyId = window.requestAnimationFrame(() => {
- setHarnessReady(true);
- });
+ const readyId = markHarnessReady(setHarnessReady);
return () => {
window.cancelAnimationFrame(readyId);
diff --git a/src/stories/components/InteractionHarness/InteractionHarness.tsx b/src/stories/components/InteractionHarness/InteractionHarness.tsx
index 61c8d25..3535ee6 100644
--- a/src/stories/components/InteractionHarness/InteractionHarness.tsx
+++ b/src/stories/components/InteractionHarness/InteractionHarness.tsx
@@ -1,7 +1,15 @@
import React from 'react';
import { AtomTrigger } from '../../../index';
import type { AtomTriggerEvent } from '../../../index';
-import { addHarnessEvent, CounterPanel, type InteractionHarnessProps } from './shared';
+import {
+ addHarnessEvent,
+ CounterPanel,
+ dispatchElementScroll,
+ markHarnessReady,
+ mockElementRect,
+ runFrameSequence,
+ type InteractionHarnessProps,
+} from './shared';
export function InteractionHarness({
once = false,
@@ -36,28 +44,20 @@ export function InteractionHarness({
return;
}
- Object.defineProperty(verticalRoot, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(0, 0, 200, 180),
- });
- Object.defineProperty(horizontalRoot, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(0, 0, 200, 120),
- });
- Object.defineProperty(verticalSentinel, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(0, 260 - verticalScrollTopRef.current, 10, 10),
- });
- Object.defineProperty(horizontalSentinel, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(260 - horizontalScrollLeftRef.current, 0, 2, 160),
- });
+ mockElementRect(verticalRoot, () => new DOMRect(0, 0, 200, 180));
+ mockElementRect(horizontalRoot, () => new DOMRect(0, 0, 200, 120));
+ mockElementRect(
+ verticalSentinel,
+ () => new DOMRect(0, 260 - verticalScrollTopRef.current, 10, 10),
+ );
+ mockElementRect(
+ horizontalSentinel,
+ () => new DOMRect(260 - horizontalScrollLeftRef.current, 0, 2, 160),
+ );
verticalScrollTopRef.current = initialVerticalScrollTop;
horizontalScrollLeftRef.current = 0;
- const readyId = window.requestAnimationFrame(() => {
- setHarnessReady(true);
- });
+ const readyId = markHarnessReady(setHarnessReady);
return () => {
window.cancelAnimationFrame(readyId);
@@ -78,11 +78,7 @@ export function InteractionHarness({
}
verticalScrollTopRef.current = nextTop;
- root.dispatchEvent(
- new root.ownerDocument.defaultView!.Event('scroll', {
- bubbles: true,
- }),
- );
+ dispatchElementScroll(root);
}, []);
const scrollHorizontal = React.useCallback((nextLeft: number) => {
@@ -92,11 +88,7 @@ export function InteractionHarness({
}
horizontalScrollLeftRef.current = nextLeft;
- root.dispatchEvent(
- new root.ownerDocument.defaultView!.Event('scroll', {
- bubbles: true,
- }),
- );
+ dispatchElementScroll(root);
}, []);
const emitEnter = React.useCallback(() => {
@@ -109,24 +101,12 @@ export function InteractionHarness({
const runVerticalSequence = React.useCallback(() => {
scrollVertical(0);
-
- window.requestAnimationFrame(() => {
- scrollVertical(120);
- window.requestAnimationFrame(() => {
- scrollVertical(280);
- });
- });
+ runFrameSequence([() => scrollVertical(120), () => scrollVertical(280)]);
}, [scrollVertical]);
const runHorizontalSequence = React.useCallback(() => {
scrollHorizontal(0);
-
- window.requestAnimationFrame(() => {
- scrollHorizontal(120);
- window.requestAnimationFrame(() => {
- scrollHorizontal(320);
- });
- });
+ runFrameSequence([() => scrollHorizontal(120), () => scrollHorizontal(320)]);
}, [scrollHorizontal]);
const resetHarness = React.useCallback(() => {
diff --git a/src/stories/components/InteractionHarness/MultiSentinelInteractionHarness.tsx b/src/stories/components/InteractionHarness/MultiSentinelInteractionHarness.tsx
index 25fb0f4..aebc792 100644
--- a/src/stories/components/InteractionHarness/MultiSentinelInteractionHarness.tsx
+++ b/src/stories/components/InteractionHarness/MultiSentinelInteractionHarness.tsx
@@ -1,7 +1,15 @@
import React from 'react';
import { AtomTrigger } from '../../../index';
import type { AtomTriggerEvent } from '../../../index';
-import { addHarnessEvent, CounterPanel, type SharedHarnessEventCallbacks } from './shared';
+import {
+ addHarnessEvent,
+ CounterPanel,
+ dispatchElementScroll,
+ markHarnessReady,
+ mockElementRect,
+ runFrameSequence,
+ type SharedHarnessEventCallbacks,
+} from './shared';
export function MultiSentinelInteractionHarness({
onEnter,
@@ -36,32 +44,15 @@ export function MultiSentinelInteractionHarness({
return;
}
- Object.defineProperty(sharedRoot, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(0, 0, 200, 180),
- });
- Object.defineProperty(horizontalFirst, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(40, 260 - scrollTopRef.current, 120, 2),
- });
- Object.defineProperty(horizontalSecond, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(40, 290 - scrollTopRef.current, 120, 2),
- });
- Object.defineProperty(verticalThird, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(260 - scrollLeftRef.current, 10, 2, 160),
- });
- Object.defineProperty(verticalFourth, 'getBoundingClientRect', {
- configurable: true,
- value: () => new DOMRect(275 - scrollLeftRef.current, 10, 2, 160),
- });
+ mockElementRect(sharedRoot, () => new DOMRect(0, 0, 200, 180));
+ mockElementRect(horizontalFirst, () => new DOMRect(40, 260 - scrollTopRef.current, 120, 2));
+ mockElementRect(horizontalSecond, () => new DOMRect(40, 290 - scrollTopRef.current, 120, 2));
+ mockElementRect(verticalThird, () => new DOMRect(260 - scrollLeftRef.current, 10, 2, 160));
+ mockElementRect(verticalFourth, () => new DOMRect(275 - scrollLeftRef.current, 10, 2, 160));
scrollTopRef.current = 0;
scrollLeftRef.current = 0;
- const readyId = window.requestAnimationFrame(() => {
- setHarnessReady(true);
- });
+ const readyId = markHarnessReady(setHarnessReady);
return () => {
window.cancelAnimationFrame(readyId);
@@ -75,11 +66,7 @@ export function MultiSentinelInteractionHarness({
}
scrollTopRef.current = nextTop;
- root.dispatchEvent(
- new root.ownerDocument.defaultView!.Event('scroll', {
- bubbles: true,
- }),
- );
+ dispatchElementScroll(root);
}, []);
const scrollHorizontal = React.useCallback((nextLeft: number) => {
@@ -89,33 +76,17 @@ export function MultiSentinelInteractionHarness({
}
scrollLeftRef.current = nextLeft;
- root.dispatchEvent(
- new root.ownerDocument.defaultView!.Event('scroll', {
- bubbles: true,
- }),
- );
+ dispatchElementScroll(root);
}, []);
const runVerticalSequence = React.useCallback(() => {
scrollVertical(0);
-
- window.requestAnimationFrame(() => {
- scrollVertical(120);
- window.requestAnimationFrame(() => {
- scrollVertical(320);
- });
- });
+ runFrameSequence([() => scrollVertical(120), () => scrollVertical(320)]);
}, [scrollVertical]);
const runHorizontalSequence = React.useCallback(() => {
scrollHorizontal(0);
-
- window.requestAnimationFrame(() => {
- scrollHorizontal(120);
- window.requestAnimationFrame(() => {
- scrollHorizontal(280);
- });
- });
+ runFrameSequence([() => scrollHorizontal(120), () => scrollHorizontal(280)]);
}, [scrollHorizontal]);
const resetHarness = React.useCallback(() => {
diff --git a/src/stories/components/InteractionHarness/shared.tsx b/src/stories/components/InteractionHarness/shared.tsx
index b4f0012..0f4e759 100644
--- a/src/stories/components/InteractionHarness/shared.tsx
+++ b/src/stories/components/InteractionHarness/shared.tsx
@@ -23,6 +23,41 @@ export function addHarnessEvent(
forwardEvent?.(event);
}
+export function mockElementRect(element: Element, readRect: () => DOMRectReadOnly): void {
+ Object.defineProperty(element, 'getBoundingClientRect', {
+ configurable: true,
+ value: readRect,
+ });
+}
+
+export function dispatchElementScroll(element: Element): void {
+ element.dispatchEvent(
+ new element.ownerDocument.defaultView!.Event('scroll', {
+ bubbles: true,
+ }),
+ );
+}
+
+export function markHarnessReady(
+ setHarnessReady: React.Dispatch>,
+): number {
+ return window.requestAnimationFrame(() => {
+ setHarnessReady(true);
+ });
+}
+
+export function runFrameSequence(callbacks: readonly (() => void)[]): void {
+ const [callback, ...remainingCallbacks] = callbacks;
+ if (!callback) {
+ return;
+ }
+
+ window.requestAnimationFrame(() => {
+ callback();
+ runFrameSequence(remainingCallbacks);
+ });
+}
+
export function CounterPanel({
title,
testIdPrefix,
diff --git a/tsdown.config.ts b/tsdown.config.ts
index 755cab0..094c5c4 100644
--- a/tsdown.config.ts
+++ b/tsdown.config.ts
@@ -1,6 +1,10 @@
+import { codecovRollupPlugin } from '@codecov/rollup-plugin';
import type { OutputOptions } from 'rolldown';
import { defineConfig, type UserConfig } from 'tsdown';
+const codecovToken = process.env.CODECOV_TOKEN;
+const enableBundleAnalysis = process.env.CI === 'true' && Boolean(codecovToken);
+
const baseConfig = {
entry: './src/index.ts',
outDir: 'lib',
@@ -58,6 +62,11 @@ export default defineConfig([
format: 'esm',
dts: true,
clean: true,
+ plugins: codecovRollupPlugin({
+ enableBundleAnalysis,
+ bundleName: 'react-atom-trigger',
+ uploadToken: codecovToken,
+ }),
outExtensions: ({ format }) => getOutExtensions(format),
outputOptions: (options, format) => withReactGlobals(options, format),
},