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 [![codecov](https://codecov.io/gh/innrvoice/react-atom-trigger/branch/master/graph/badge.svg)](https://codecov.io/gh/innrvoice/react-atom-trigger) -[![bundle size](https://badgen.net/bundlejs/minzip/react-atom-trigger)](https://bundlejs.com/?q=react-atom-trigger) +[![bundle size](https://codecov.io/github/innrvoice/react-atom-trigger/branch/master/graph/bundle/react-atom-trigger-esm/badge.svg)](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), },