react-atom-trigger 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,12 +1,12 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2022 innrVoice
3
+ Copyright (c) 2022 Pavel Bochkov-Rastopchin
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
7
7
  in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
8
+ to use, copy, modify, merge, publish, distribute, sublicense and/or sell
9
+ copies of the Software and to permit persons to whom the Software is
10
10
  furnished to do so, subject to the following conditions:
11
11
 
12
12
  The above copyright notice and this permission notice shall be included in all
package/MIGRATION.md ADDED
@@ -0,0 +1,255 @@
1
+ # Migration Guide: v1 to v2
2
+
3
+ `react-atom-trigger@2` is a breaking release.
4
+
5
+ The biggest change is simple: `AtomTrigger` does its own observation now. In `v1.x` you had to pass scroll state and dimensions in from the outside. In `v2.x` you pass callbacks and, when needed, a root element.
6
+
7
+ ## Before you start
8
+
9
+ This is not just a prop rename release.
10
+
11
+ Please re-check the behavior in the real UI after upgrading, especially if you use:
12
+
13
+ - `threshold`
14
+ - `rootMargin`
15
+ - custom scroll containers
16
+ - any code that was sensitive to the exact timing of the old callback
17
+
18
+ The new version samples geometry internally, so some edge timing can feel a bit different near boundaries.
19
+
20
+ ## Quick prop map
21
+
22
+ | v1.x | v2.x |
23
+ | -------------------- | ----------------------------------------- |
24
+ | `callback` | `onEnter`, `onLeave` or `onEvent` |
25
+ | `behavior="enter"` | `onEnter` |
26
+ | `behavior="leave"` | `onLeave` |
27
+ | `behavior="default"` | `onEvent` or both `onEnter` and `onLeave` |
28
+ | `triggerOnce` | `once` or `oncePerDirection` |
29
+ | `scrollEvent` | removed |
30
+ | `dimensions` | removed |
31
+ | `offset` | `rootMargin` |
32
+ | `getDebugInfo` | read data from `AtomTriggerEvent` |
33
+ | `IAtomTriggerProps` | `AtomTriggerProps` |
34
+
35
+ ## Hook and type changes
36
+
37
+ | v1.x | v2.x |
38
+ | -------------------------------------- | --------------------------------------------- |
39
+ | `useWindowScroll` | `useScrollPosition()` |
40
+ | `useContainerScroll({ containerRef })` | `useScrollPosition({ target: containerRef })` |
41
+ | `useWindowDimensions` | `useViewportSize()` |
42
+ | `Options` | `ListenerOptions` |
43
+ | `ScrollEvent` | removed |
44
+ | `Dimensions` | removed |
45
+ | `DebugInfo` | removed |
46
+ | `log` | removed |
47
+
48
+ ## Common upgrades
49
+
50
+ ### 1. Simple enter trigger
51
+
52
+ #### v1.x
53
+
54
+ ```tsx
55
+ import React from 'react';
56
+ import { AtomTrigger, useWindowDimensions, useWindowScroll } from 'react-atom-trigger';
57
+
58
+ export function HeroAnimationTrigger() {
59
+ const scrollEvent = useWindowScroll();
60
+ const dimensions = useWindowDimensions();
61
+
62
+ return (
63
+ <AtomTrigger
64
+ behavior="enter"
65
+ callback={() => {
66
+ console.log('start animation');
67
+ }}
68
+ scrollEvent={scrollEvent}
69
+ dimensions={dimensions}
70
+ />
71
+ );
72
+ }
73
+ ```
74
+
75
+ #### v2.x
76
+
77
+ ```tsx
78
+ import React from 'react';
79
+ import { AtomTrigger } from 'react-atom-trigger';
80
+
81
+ export function HeroAnimationTrigger() {
82
+ return <AtomTrigger onEnter={() => console.log('start animation')} />;
83
+ }
84
+ ```
85
+
86
+ ### 2. Enter and leave in one place
87
+
88
+ #### v1.x
89
+
90
+ ```tsx
91
+ <AtomTrigger
92
+ behavior="default"
93
+ callback={() => {
94
+ console.log('visibility changed');
95
+ }}
96
+ scrollEvent={scrollEvent}
97
+ dimensions={dimensions}
98
+ />
99
+ ```
100
+
101
+ #### v2.x
102
+
103
+ ```tsx
104
+ <AtomTrigger
105
+ onEvent={event => {
106
+ console.log(event.type, event.position, event.counts);
107
+ }}
108
+ />
109
+ ```
110
+
111
+ ### 3. Custom scroll container
112
+
113
+ #### v1.x
114
+
115
+ ```tsx
116
+ import React from 'react';
117
+ import { AtomTrigger, useContainerScroll } from 'react-atom-trigger';
118
+
119
+ export function ContainerExample() {
120
+ const containerRef = React.useRef<HTMLDivElement>(null);
121
+ const scrollEvent = useContainerScroll({ containerRef });
122
+
123
+ return (
124
+ <div ref={containerRef} style={{ height: 320, overflowY: 'auto' }}>
125
+ <div style={{ height: 600 }} />
126
+ <AtomTrigger
127
+ behavior="enter"
128
+ callback={() => {
129
+ console.log('entered container viewport');
130
+ }}
131
+ scrollEvent={scrollEvent}
132
+ dimensions={{ width: 320, height: 320 }}
133
+ />
134
+ <div style={{ height: 600 }} />
135
+ </div>
136
+ );
137
+ }
138
+ ```
139
+
140
+ #### v2.x
141
+
142
+ ```tsx
143
+ import React from 'react';
144
+ import { AtomTrigger } from 'react-atom-trigger';
145
+
146
+ export function ContainerExample() {
147
+ const containerRef = React.useRef<HTMLDivElement>(null);
148
+
149
+ return (
150
+ <div ref={containerRef} style={{ height: 320, overflowY: 'auto' }}>
151
+ <div style={{ height: 600 }} />
152
+ <AtomTrigger
153
+ rootRef={containerRef}
154
+ onEnter={() => {
155
+ console.log('entered container viewport');
156
+ }}
157
+ />
158
+ <div style={{ height: 600 }} />
159
+ </div>
160
+ );
161
+ }
162
+ ```
163
+
164
+ If you render the container in JSX, `rootRef` is usually the right choice.
165
+
166
+ If you already have the DOM element instance from somewhere else, use `root`.
167
+
168
+ If you want normal viewport behavior, pass neither.
169
+
170
+ ### 4. `offset` to `rootMargin`
171
+
172
+ This part needs a little attention.
173
+
174
+ `offset` and `rootMargin` are related, but not identical in meaning. If you were using:
175
+
176
+ ```tsx
177
+ <AtomTrigger offset={[100, 0, 0, 0]} />
178
+ ```
179
+
180
+ the usual `v2.x` equivalent is:
181
+
182
+ ```tsx
183
+ <AtomTrigger rootMargin="-100px 0px 0px 0px" />
184
+ ```
185
+
186
+ For pixel tuples, this also works:
187
+
188
+ ```tsx
189
+ <AtomTrigger rootMargin={[-100, 0, 0, 0]} />
190
+ ```
191
+
192
+ After migrating, please check it in the actual UI. `rootMargin` is the place where timing differences are easiest to notice.
193
+
194
+ ### 5. Replacing `getDebugInfo`
195
+
196
+ #### v1.x
197
+
198
+ ```tsx
199
+ <AtomTrigger
200
+ callback={handleTrigger}
201
+ getDebugInfo={info => {
202
+ console.log(info.trigger, info.timesTriggered);
203
+ }}
204
+ scrollEvent={scrollEvent}
205
+ dimensions={dimensions}
206
+ />
207
+ ```
208
+
209
+ #### v2.x
210
+
211
+ ```tsx
212
+ <AtomTrigger
213
+ onEvent={event => {
214
+ console.log(event.type);
215
+ console.log(event.counts);
216
+ console.log(event.position);
217
+ console.log(event.movementDirection);
218
+ console.log(event.entry.intersectionRatio);
219
+ }}
220
+ />
221
+ ```
222
+
223
+ ## Small hook examples
224
+
225
+ ### Replace `useWindowScroll`
226
+
227
+ ```tsx
228
+ const position = useScrollPosition();
229
+ console.log(position.y);
230
+ ```
231
+
232
+ ### Replace `useContainerScroll`
233
+
234
+ ```tsx
235
+ const containerRef = React.useRef<HTMLDivElement>(null);
236
+ const position = useScrollPosition({ target: containerRef });
237
+ console.log(position.y);
238
+ ```
239
+
240
+ ### Replace `useWindowDimensions`
241
+
242
+ ```tsx
243
+ const viewport = useViewportSize();
244
+ console.log(viewport.height);
245
+ ```
246
+
247
+ ## Final check
248
+
249
+ Your migration is probably done when all of these are true:
250
+
251
+ 1. No `AtomTrigger` still passes `scrollEvent`, `dimensions`, `behavior`, `callback`, `getDebugInfo`, `triggerOnce` or `offset`.
252
+ 2. Trigger handlers now use `onEnter`, `onLeave` and/or `onEvent`.
253
+ 3. Custom containers use `root` or `rootRef`.
254
+ 4. Hook imports were moved to `useScrollPosition` and `useViewportSize`.
255
+ 5. You checked the real UI, not only TypeScript errors, especially around `threshold` and `rootMargin`.
package/README.md CHANGED
@@ -1,84 +1,302 @@
1
1
  # react-atom-trigger
2
2
 
3
- AtomTrigger helps solve the problem of executing code when some element "scrolls into (or out of) view". A pretty simple "[react-waypoint](https://www.npmjs.com/package/react-waypoint)" alternative written in Typescript.
3
+ `react-atom-trigger` helps with the usual "run some code when this thing enters or leaves view" problem.
4
+ It is a lightweight React alternative to `react-waypoint`, written in TypeScript.
4
5
 
5
- ## Basic features
6
+ ## v2 is a breaking release
6
7
 
8
+ If you are coming from `v1.x`, please check [MIGRATION.md](./MIGRATION.md).
7
9
 
8
- Exposes `<AtomTrigger {...props} />` component, where `props` are:
10
+ If you want to stay on the old API:
9
11
 
12
+ ```bash
13
+ # pnpm
14
+ pnpm add react-atom-trigger@^1
15
+
16
+ # npm
17
+ npm install react-atom-trigger@^1
18
+
19
+ # yarn
20
+ yarn add react-atom-trigger@^1
10
21
  ```
11
- interface IAtomTriggerProps {
12
- scrollEvent: ScrollEvent;
13
- dimensions: Dimensions;
14
- behavior?: 'default' | 'enter' | 'leave';
15
- callback: () => unknown;
16
- getDebugInfo?: (data: DebugInfo) => void;
17
- triggerOnce?: boolean;
18
- className?: string;
19
- offset?: [number, number, number, number];
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ # pnpm
27
+ pnpm add react-atom-trigger
28
+
29
+ # npm
30
+ npm install react-atom-trigger
31
+
32
+ # yarn
33
+ yarn add react-atom-trigger
34
+ ```
35
+
36
+ ## How it works
37
+
38
+ `react-atom-trigger` uses a mixed approach.
39
+
40
+ - Geometry is the real source of truth for `enter` and `leave`.
41
+ - `IntersectionObserver` is only there to wake things up when the browser notices a layout shift.
42
+ - `rootMargin` logic is handled by the library itself, so it stays consistent and does not depend on native observer quirks.
43
+
44
+ In practice this means `AtomTrigger` reacts to:
45
+
46
+ - scroll
47
+ - window resize
48
+ - root resize
49
+ - sentinel resize
50
+ - layout shifts that move the observed element even if no scroll event happened
51
+
52
+ This is the main reason `v2` can support custom margin-aware behavior and still react to browser-driven layout changes.
53
+
54
+ ## Quick start
55
+
56
+ ```tsx
57
+ import React from 'react';
58
+ import { AtomTrigger } from 'react-atom-trigger';
59
+
60
+ export function Example() {
61
+ return (
62
+ <AtomTrigger
63
+ onEnter={event => {
64
+ console.log('entered', event);
65
+ }}
66
+ onLeave={event => {
67
+ console.log('left', event);
68
+ }}
69
+ rootMargin="0px 0px 160px 0px"
70
+ oncePerDirection
71
+ />
72
+ );
20
73
  }
21
74
  ```
22
75
 
23
- In order to "work" `AtomTrigger` needs callback, dimensions and simple scroll event data provided.
76
+ If you want an already-visible trigger to behave like a normal first `enter`, pass
77
+ `fireOnInitialVisible`.
24
78
 
25
- ### Callback
79
+ ```tsx
80
+ import React from 'react';
81
+ import { AtomTrigger } from 'react-atom-trigger';
26
82
 
27
- The function to be executed when AtomTrigger enters or leaves some container.
83
+ export function RestoredScrollExample() {
84
+ return (
85
+ <AtomTrigger
86
+ fireOnInitialVisible
87
+ onEnter={event => {
88
+ if (event.isInitial) {
89
+ console.log('started visible after load');
90
+ return;
91
+ }
28
92
 
93
+ console.log('entered from scrolling');
94
+ }}
95
+ />
96
+ );
97
+ }
29
98
  ```
30
- callback: () => unknown;
99
+
100
+ ## Child mode
101
+
102
+ If you pass one top-level child, `AtomTrigger` observes that element directly instead of rendering its own sentinel.
103
+
104
+ ```tsx
105
+ import React from 'react';
106
+ import { AtomTrigger } from 'react-atom-trigger';
107
+
108
+ export function HeroTrigger() {
109
+ return (
110
+ <AtomTrigger threshold={0.75} onEnter={() => console.log('hero is mostly visible')}>
111
+ <section style={{ minHeight: 240 }}>Hero content</section>
112
+ </AtomTrigger>
113
+ );
114
+ }
115
+ ```
116
+
117
+ This is usually the better mode when `threshold` should depend on a real element size.
118
+
119
+ Intrinsic elements such as `<div>` and `<section>` work automatically.
120
+
121
+ If you use a custom component, the ref that `AtomTrigger` passes down still has to reach a real DOM
122
+ element:
123
+
124
+ - in React 19, the component can receive `ref` as a prop and pass it through
125
+ - in React 18 and older, use `React.forwardRef`
126
+
127
+ If the ref never reaches a DOM node, child mode cannot observe anything.
128
+
129
+ ## API
130
+
131
+ ```ts
132
+ interface AtomTriggerProps {
133
+ onEnter?: (event: AtomTriggerEvent) => void;
134
+ onLeave?: (event: AtomTriggerEvent) => void;
135
+ onEvent?: (event: AtomTriggerEvent) => void;
136
+ children?: React.ReactNode;
137
+ once?: boolean;
138
+ oncePerDirection?: boolean;
139
+ fireOnInitialVisible?: boolean;
140
+ disabled?: boolean;
141
+ threshold?: number;
142
+ root?: Element | null;
143
+ rootRef?: React.RefObject<Element | null>;
144
+ rootMargin?: string | [number, number, number, number];
145
+ className?: string;
146
+ }
31
147
  ```
32
148
 
149
+ ### Props in short
33
150
 
34
- ### Dimensions
151
+ - `onEnter`, `onLeave`, `onEvent`: trigger callbacks with a rich event payload.
152
+ - `children`: observe one real child element instead of the internal sentinel.
153
+ - `once`: allow only the first transition overall.
154
+ - `oncePerDirection`: allow one `enter` and one `leave`.
155
+ - `fireOnInitialVisible`: emit an initial `enter` when observation starts and the trigger is already active.
156
+ - `disabled`: stop observing without unmounting the component.
157
+ - `threshold`: a number from `0` to `1`. It affects `enter`, not `leave`.
158
+ - `root`: use a specific DOM element as the visible area.
159
+ - `rootRef`: same idea as `root`, but better when the container is created in JSX. If both are passed, `rootRef` wins.
160
+ - `rootMargin`: expand or shrink the effective root. String values use `IntersectionObserver`-style syntax. A four-number array is treated as `[top, right, bottom, left]` in pixels.
161
+ - `className`: applies only to the internal sentinel.
35
162
 
36
- Dimensions of the main "container" (window in many cases).
163
+ ## Event payload
37
164
 
165
+ ```ts
166
+ type AtomTriggerEvent = {
167
+ type: 'enter' | 'leave';
168
+ isInitial: boolean;
169
+ entry: AtomTriggerEntry;
170
+ counts: {
171
+ entered: number;
172
+ left: number;
173
+ };
174
+ movementDirection: 'up' | 'down' | 'left' | 'right' | 'stationary' | 'unknown';
175
+ position: 'inside' | 'above' | 'below' | 'left' | 'right' | 'outside';
176
+ timestamp: number;
177
+ };
38
178
  ```
39
- type Dimensions = {
40
- width: number;
41
- height: number;
179
+
180
+ ```ts
181
+ type AtomTriggerEntry = {
182
+ target: Element;
183
+ rootBounds: DOMRectReadOnly | null;
184
+ boundingClientRect: DOMRectReadOnly;
185
+ intersectionRect: DOMRectReadOnly;
186
+ isIntersecting: boolean;
187
+ intersectionRatio: number;
188
+ source: 'geometry';
42
189
  };
43
190
  ```
44
191
 
45
- So if you have some logic of calculating container size and container resize handling, just provide needed data to AtomTrigger.
192
+ The payload is library-owned geometry data. It is not a native `IntersectionObserverEntry`.
193
+
194
+ `isInitial` is `true` only for the synthetic first `enter` created by
195
+ `fireOnInitialVisible`.
46
196
 
47
- ### Scroll Event
48
-
49
- To trigger "events" `AtomTrigger` needs some kind of simple scroll event provided.
197
+ ## Hooks
50
198
 
199
+ For someone who wants everything out-of-the-box, `useScrollPosition` and `useViewportSize` are also available.
200
+
201
+ ```ts
202
+ useScrollPosition(options?: {
203
+ target?: Window | HTMLElement | React.RefObject<HTMLElement | null>;
204
+ passive?: boolean;
205
+ throttleMs?: number;
206
+ enabled?: boolean;
207
+ }): { x: number; y: number }
51
208
  ```
52
- type ScrollEvent = {
53
- scrollX: number;
54
- scrollY: number;
55
- };
209
+
210
+ ```ts
211
+ useViewportSize(options?: {
212
+ passive?: boolean;
213
+ throttleMs?: number;
214
+ enabled?: boolean;
215
+ }): { width: number; height: number }
56
216
  ```
57
217
 
58
- So, if you already have some scroll event listener, just provide it to AtomTrigger.
218
+ Both hooks are SSR-safe. Default throttling is `16ms`.
59
219
 
60
- ## Utility hooks
61
- For someone who wants everything out-of-the-box, `useWindowDimensions`, `useWindowScroll` and `useContainerScroll` hooks are also available for import.
220
+ ## Notes
62
221
 
63
- ## Build
64
- This package is built with `tsdown`.
222
+ - In sentinel mode, `threshold` is usually only interesting if your sentinel has real width or height. The default sentinel is almost point-like.
223
+ - Child mode needs exactly one top-level child and any custom component used there needs to pass the received ref through to a DOM element.
224
+ - In React 19, a plain function component can also work in child mode if it passes the received `ref` prop through to a DOM element.
225
+ - `rootMargin` is handled by the library geometry logic. `IntersectionObserver` is only used as a wake-up signal for layout shifts.
226
+
227
+ ## Migration from v1
228
+
229
+ The short version:
230
+
231
+ 1. `callback` became `onEnter`, `onLeave` and `onEvent`.
232
+ 2. `behavior` is gone.
233
+ 3. `triggerOnce` became `once` or `oncePerDirection`.
234
+ 4. `scrollEvent`, `dimensions` and `offset` are gone.
235
+ 5. `useWindowScroll` / `useContainerScroll` became `useScrollPosition`.
236
+ 6. `useWindowDimensions` became `useViewportSize`.
237
+
238
+ For the real upgrade notes and examples, see [MIGRATION.md](./MIGRATION.md).
239
+
240
+ ## Build output
65
241
 
66
- Build output:
242
+ This package is built with `tsdown`.
67
243
 
68
244
  ```text
69
245
  lib/index.js
70
- lib/index.es.js
246
+ lib/index.umd.js
71
247
  lib/index.d.ts
72
248
  ```
73
249
 
74
- ## UMD global
75
250
  When the UMD bundle is loaded directly in the browser, the library is exposed as `window.reactAtomTrigger`.
76
251
 
77
252
  ## Examples
78
- It is sometimes better to tweak and see for yourself: [CodeSandbox examples](https://codesandbox.io/examples/package/react-atom-trigger).
79
253
 
80
- [**More detailed react-atom-trigger overview with examples**](https://visiofutura.com/solving-scroll-into-view-problem-in-react-my-way-a8056a1bdc11)
254
+ ### Storybook
255
+
256
+ Storybook is the easiest way to see how the component behaves.
257
+
258
+ - `AtomTrigger Demo`: regular usage examples.
259
+ - `Extended Demo`: a larger animated interaction demo that shows AtomTrigger driving scene changes,
260
+ event timing and more realistic scroll-based UI behavior.
261
+ - `Internal Tests`: interaction stories used for local checks and Storybook tests.
262
+
263
+ To run Storybook locally:
264
+
265
+ ```bash
266
+ pnpm storybook
267
+ ```
268
+
269
+ ### CodeSandbox
270
+
271
+ Quick way to tweak it in the browser.
81
272
 
273
+ - [Basic sentinel example](https://codesandbox.io/p/sandbox/react-atom-trigger-v2-basic-example-9xrzmg)
274
+ - [Child mode threshold example](https://codesandbox.io/p/sandbox/react-atom-trigger-v2-child-mode-threshold-qcpv28)
275
+ - [Fixed header offset example](https://codesandbox.io/p/devbox/react-atom-trigger-v2-fixed-header-offset-62lmrv)
276
+ - [Initial visible on load example](https://codesandbox.io/p/devbox/react-atom-trigger-v2-initial-visible-on-load-ncqjtf)
277
+ - [Horizontal scroll container example](https://codesandbox.io/p/devbox/react-atom-trigger-v2-horizontal-scroll-container-hs33gq)
278
+
279
+ ## Development
280
+
281
+ ```bash
282
+ pnpm install
283
+ pnpm lint
284
+ pnpm test
285
+ pnpm test:storybook
286
+ pnpm build
287
+ pnpm format:check
288
+ ```
289
+
290
+ ## Storybook (Static Build)
291
+
292
+ Build:
293
+
294
+ ```bash
295
+ pnpm build:sb
296
+ ```
82
297
 
298
+ Output:
83
299
 
300
+ `storybook-static/`
84
301
 
302
+ This directory is used for deployment to `storybook.atomtrigger.dev`.
package/lib/index.d.ts CHANGED
@@ -1,49 +1,69 @@
1
1
  import React from "react";
2
2
 
3
3
  //#region src/AtomTrigger.types.d.ts
4
- type ScrollEvent = {
5
- scrollX: number;
6
- scrollY: number;
4
+ type MovementDirection = 'up' | 'down' | 'left' | 'right' | 'stationary' | 'unknown';
5
+ type TriggerPosition = 'inside' | 'above' | 'below' | 'left' | 'right' | 'outside';
6
+ type TriggerType = 'enter' | 'leave';
7
+ type TriggerCounts = {
8
+ entered: number;
9
+ left: number;
7
10
  };
8
- type Dimensions = {
9
- width: number;
10
- height: number;
11
+ type RootMarginTuple = readonly [number, number, number, number];
12
+ type AtomTriggerEntry = {
13
+ target: Element;
14
+ rootBounds: DOMRectReadOnly | null;
15
+ boundingClientRect: DOMRectReadOnly;
16
+ intersectionRect: DOMRectReadOnly;
17
+ isIntersecting: boolean;
18
+ intersectionRatio: number;
19
+ source: 'geometry';
11
20
  };
12
- type DebugInfo = {
13
- timesTriggered: {
14
- leftViewport: number;
15
- enteredViewport: number;
16
- };
17
- trigger: 'entered' | 'left';
21
+ type AtomTriggerEvent = {
22
+ type: TriggerType;
23
+ isInitial: boolean;
24
+ entry: AtomTriggerEntry;
25
+ counts: TriggerCounts;
26
+ movementDirection: MovementDirection;
27
+ position: TriggerPosition;
28
+ timestamp: number;
18
29
  };
19
- interface IAtomTriggerProps {
20
- scrollEvent: ScrollEvent;
21
- behavior?: 'default' | 'enter' | 'leave';
22
- callback: () => unknown;
23
- getDebugInfo?: (data: DebugInfo) => void;
24
- triggerOnce?: boolean;
30
+ interface AtomTriggerProps {
31
+ onEnter?: (event: AtomTriggerEvent) => void;
32
+ onLeave?: (event: AtomTriggerEvent) => void;
33
+ onEvent?: (event: AtomTriggerEvent) => void;
34
+ children?: React.ReactNode;
35
+ once?: boolean;
36
+ oncePerDirection?: boolean;
37
+ fireOnInitialVisible?: boolean;
38
+ disabled?: boolean;
39
+ threshold?: number;
40
+ root?: Element | null;
41
+ rootRef?: React.RefObject<Element | null>;
42
+ rootMargin?: string | RootMarginTuple;
25
43
  className?: string;
26
- dimensions: Dimensions;
27
- offset?: [number, number, number, number];
28
44
  }
29
45
  //#endregion
30
46
  //#region src/AtomTrigger.d.ts
31
- declare const AtomTrigger: React.FC<IAtomTriggerProps>;
47
+ declare const AtomTrigger: React.FC<AtomTriggerProps>;
32
48
  //#endregion
33
49
  //#region src/utils.d.ts
34
- type Options = {
35
- passiveEventListener?: boolean;
36
- eventListenerTimeoutMs?: number;
50
+ type ScrollPosition = {
51
+ x: number;
52
+ y: number;
53
+ };
54
+ type ViewportSize = {
55
+ width: number;
56
+ height: number;
57
+ };
58
+ type ListenerOptions = {
59
+ passive?: boolean;
60
+ throttleMs?: number;
61
+ enabled?: boolean;
62
+ };
63
+ type UseScrollPositionOptions = ListenerOptions & {
64
+ target?: Window | HTMLElement | React.RefObject<HTMLElement | null>;
37
65
  };
38
- declare function log<T>(log: T, color?: string): void;
39
- declare function useWindowDimensions(options?: Options | undefined): Dimensions;
40
- declare function useContainerScroll({
41
- containerRef,
42
- options
43
- }: {
44
- containerRef?: React.RefObject<HTMLDivElement>;
45
- options?: Options;
46
- }): ScrollEvent;
47
- declare function useWindowScroll(options?: Options): ScrollEvent;
66
+ declare function useViewportSize(options?: ListenerOptions): ViewportSize;
67
+ declare function useScrollPosition(options?: UseScrollPositionOptions): ScrollPosition;
48
68
  //#endregion
49
- export { AtomTrigger, Options, log, useContainerScroll, useWindowDimensions, useWindowScroll };
69
+ export { AtomTrigger, type AtomTriggerEntry, type AtomTriggerEvent, type AtomTriggerProps, type ListenerOptions, type MovementDirection, type RootMarginTuple, type ScrollPosition, type TriggerCounts, type TriggerPosition, type TriggerType, type UseScrollPositionOptions, type ViewportSize, useScrollPosition, useViewportSize };
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require(`react`)):typeof define==`function`&&define.amd?define([`exports`,`react`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.reactAtomTrigger={},e.React))})(this,function(e,t){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var n=Object.create,r=Object.defineProperty,i=Object.getOwnPropertyDescriptor,a=Object.getOwnPropertyNames,o=Object.getPrototypeOf,s=Object.prototype.hasOwnProperty,c=(e,t,n,o)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=a(t),l=0,u=c.length,d;l<u;l++)d=c[l],!s.call(e,d)&&d!==n&&r(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(o=i(t,d))||o.enumerable});return e};t=((e,t,i)=>(i=e==null?{}:n(o(e)),c(t||!e||!e.__esModule?r(i,`default`,{value:e,enumerable:!0}):i,e)))(t);let l=({scrollEvent:e,callback:n,getDebugInfo:r,triggerOnce:i=!1,className:a,behavior:o=`default`,dimensions:s,offset:c=[0,0,0,0]})=>{let l=t.default.useRef(null),[u,d]=t.default.useState(void 0),f=t.default.useRef(void 0),[p,m]=t.default.useState({leftViewport:0,enteredViewport:0});return t.default.useLayoutEffect(()=>{if(l.current){let e=l.current.getBoundingClientRect(),[t,n,r,i]=c;e.top>t&&e.bottom<s.height-r&&e.left>i&&e.right<s.width-n?d(`inViewport`):e.top>s.height-r?d(`bottom`):d(`top`)}},[l,e,s,c]),t.default.useLayoutEffect(()=>{if(f.current===void 0&&u!==void 0&&(f.current=u),u===`inViewport`&&(f.current===`bottom`||f.current===`top`)){if(o===`enter`&&(!i||i&&p.enteredViewport)||o===`default`&&(!i||i&&(p.enteredViewport<1||p.leftViewport<1))){n&&n();let e={...p,enteredViewport:p.enteredViewport+1};r&&r({timesTriggered:e,trigger:`entered`}),m(e)}f.current=u}if((u===`top`||u===`bottom`)&&f.current===`inViewport`&&(f.current=u,o===`leave`&&(!i||i&&p.leftViewport===0)||o===`default`&&(!i||i&&(p.leftViewport<1||p.enteredViewport<1)))){n&&n();let e={...p,leftViewport:p.leftViewport+1};r&&r({timesTriggered:e,trigger:`left`}),m(e)}},[u,n,i,o,r]),t.default.createElement(`div`,{ref:l,style:{display:`table`},className:a})};function u(e,t){}function d(){let{scrollX:e,scrollY:t}=window;return{scrollX:e,scrollY:t}}function f(){let{innerWidth:e,innerHeight:t}=window;return{width:e,height:t}}function p(e){let[n,r]=t.default.useState(f()),i=t.default.useRef(null),a=t.default.useRef(!1),o=e?.eventListenerTimeoutMs||15;return t.default.useEffect(()=>{r(f());function t(){i.current&&clearTimeout(i.current),i.current=setTimeout(()=>r(f()),o)}return window.addEventListener(`resize`,t,{passive:e?.passiveEventListener}),a.current=!0,()=>{a&&window.removeEventListener(`resize`,t)}},[]),n}function m({containerRef:e,options:n}){let[r,i]=t.default.useState(d()),a=t.default.useRef(null),o=t.default.useRef(!1);return t.default.useEffect(()=>{let t=e=>{let t=e.target;a.current&&clearTimeout(a.current),a.current=setTimeout(()=>{i({scrollX:t.scrollLeft,scrollY:t.scrollTop})},n?.eventListenerTimeoutMs||15)},r=e?.current;return r&&(r&&o.current===!1&&r.addEventListener(`scroll`,t,{passive:n?.passiveEventListener}),o.current=!0),()=>{o&&r&&r.removeEventListener(`scroll`,t)}},[e]),r}function h(e){let[n,r]=t.default.useState(d()),i=t.default.useRef(null),a=t.default.useRef(!1);return t.default.useEffect(()=>{let t=()=>{i.current&&clearTimeout(i.current),i.current=setTimeout(()=>{let{scrollX:e,scrollY:t}=d();r({scrollX:e,scrollY:t})},e?.eventListenerTimeoutMs||20)};return window.addEventListener(`scroll`,t,{passive:e?.passiveEventListener}),a.current=!0,()=>{a&&window.removeEventListener(`scroll`,t)}},[]),n}e.AtomTrigger=l,e.log=u,e.useContainerScroll=m,e.useWindowDimensions=p,e.useWindowScroll=h});
1
+ import e from"react";const t=new Set;function n(){if(typeof process>`u`||!process.env)return null;let e=`production`;return e===`development`||e===`production`?e:null}function r(e){n()===`development`&&(t.has(e)||(t.add(e),typeof console<`u`&&console.warn&&console.warn(e)))}const i=Symbol.for(`react.forward_ref`),a=Symbol.for(`react.memo`);function o(e,t){if(e){if(typeof e==`function`){e(t);return}e.current=t}}function s(e){if(typeof e==`string`||typeof e==`function`)return!0;if(typeof e!=`object`||!e)return!1;let t=e;return t.$$typeof===i?!0:t.$$typeof===a&&t.type?s(t.type):!1}function c(t,n,r,i){return t?n===1?r?i===e.Fragment?`[react-atom-trigger] Child mode does not support React.Fragment. Wrap the content in a single DOM element. Observation is disabled for this render.`:(!i||s(i),null):`[react-atom-trigger] Child mode expects a React element child. Observation is disabled for this render.`:`[react-atom-trigger] Child mode expects exactly one top-level React element. Observation is disabled for this render.`:null}function l(e){return Array.isArray(e)&&e.length===4&&e.every(e=>typeof e==`number`&&Number.isFinite(e))}function u(e){return typeof e==`string`?e:l(e)?e.map(e=>`${Object.is(e,-0)?0:e}px`).join(` `):(e==null||r(`[react-atom-trigger] Invalid rootMargin array ${JSON.stringify(e)}. Use exactly four finite numbers: [top, right, bottom, left]. Falling back to 0px.`),`0px`)}function d(e,t){let n=e.trim();if(!n||/^[+-]?0(?:\.0+)?$/.test(n))return 0;let i=n.match(/^([+-]?(?:\d+\.?\d*|\.\d+))(px|%)$/);if(!i)return r(`[react-atom-trigger] Invalid rootMargin token "${n}". Use px, % or 0. Falling back to 0px.`),0;let[,a,o]=i,s=Number.parseFloat(a);return o===`%`?s/100*t:s}function f(e,t,n){let i=e.trim().split(/\s+/).filter(Boolean);if(i.length>4)return r(`[react-atom-trigger] Invalid rootMargin "${e}". Use 1 to 4 values in IntersectionObserver order. Falling back to 0px.`),{top:0,right:0,bottom:0,left:0};let[a=`0px`,o=a,s=a,c=o]=i;return{top:d(a,n),right:d(o,t),bottom:d(s,n),left:d(c,t)}}function p(e,t){let n=f(t,e.width,e.height);return new DOMRect(e.left-n.left,e.top-n.top,e.width+n.left+n.right,e.height+n.top+n.bottom)}function m(e){let t=e.width>0?e.width:1,n=e.height>0?e.height:1;return t===e.width&&n===e.height?e:new DOMRect(e.left,e.top,t,n)}function h(e,t){let n=Math.max(e.left,t.left),r=Math.max(e.top,t.top),i=Math.min(e.right,t.right),a=Math.min(e.bottom,t.bottom);return i<=n||a<=r?new DOMRect(0,0,0,0):new DOMRect(n,r,i-n,a-r)}function g(e,t){let n=e.width*e.height;return n<=0?0:t.width*t.height/n}function _(e){return Math.min(1,Math.max(0,e))}function v(e){return Array.isArray(e)?(r("[react-atom-trigger] `threshold` expects a single number in v2. Using the first finite numeric entry."),_(e.find(e=>typeof e==`number`&&Number.isFinite(e))??0)):e==null?0:typeof e!=`number`||!Number.isFinite(e)?(r("[react-atom-trigger] `threshold` must be a finite number between 0 and 1. Falling back to 0."),0):((e<0||e>1)&&r("[react-atom-trigger] `threshold` should be between 0 and 1. Values are clamped."),_(e))}function y(e,t){if(!e)return`unknown`;let n=t.left-e.left,r=t.top-e.top;return n===0&&r===0?`stationary`:Math.abs(r)>=Math.abs(n)?r<0?`up`:`down`:n<0?`left`:`right`}function b(e){if(e.isIntersecting)return`inside`;let t=e.boundingClientRect,n=e.rootBounds?.top??0,r=e.rootBounds?.bottom??window.innerHeight,i=e.rootBounds?.left??0,a=e.rootBounds?.right??window.innerWidth;return t.bottom<=n?`above`:t.top>=r?`below`:t.right<=i?`left`:t.left>=a?`right`:`outside`}const x=new WeakMap;function S(){return new DOMRect(0,0,window.innerWidth,window.innerHeight)}function C(e){return e===window||typeof Window<`u`&&e instanceof Window}function w(e,t,n,r){return!(n&&t.entered+t.left>0||r&&(e===`enter`&&t.entered>0||e===`leave`&&t.left>0))}function T(e){return e.once?e.counts.entered+e.counts.left>0:e.oncePerDirection?e.counts.entered>0&&e.counts.left>0:!1}function E(e){e.previousTriggerActive=void 0,e.previousRect=null}function D(e,t){let n=m(e.node.getBoundingClientRect()),r=p(t,e.rootMargin),i=h(n,r),a=g(n,i),o=a>0,s=e.previousTriggerActive,c=s===!0||e.threshold===0?o:a>=e.threshold,l=y(e.previousRect,n);e.previousRect=n,e.previousTriggerActive=c;let u=Date.now(),d={target:e.node,rootBounds:r,boundingClientRect:n,intersectionRect:i,isIntersecting:o,intersectionRatio:a,source:`geometry`},f=s===void 0;if(f&&(!e.fireOnInitialVisible||!c)||s===c)return;let _=c?`enter`:`leave`;if(!w(_,e.counts,e.once,e.oncePerDirection))return;let v=_===`enter`?{...e.counts,entered:e.counts.entered+1}:{...e.counts,left:e.counts.left+1};e.counts=v;let x={type:_,isInitial:f,entry:d,counts:v,movementDirection:l,position:b(d),timestamp:u};e.onEvent?.(x),_===`enter`?e.onEnter?.(x):e.onLeave?.(x),T(e)&&e.dispose?.()}function O(e){let t={registrations:new Set,rafId:0,resizeObserver:null,intersectionObserver:null,queueSample:()=>{},cleanup:()=>{}},n=()=>{if(t.rafId=0,t.registrations.size===0)return;let n=C(e)?S():e.getBoundingClientRect();for(let e of t.registrations)D(e,n)},r=()=>{if(t.rafId!==0)return;t.rafId=-1;let e=window.requestAnimationFrame(()=>{t.rafId=0,n()});t.rafId===-1&&(t.rafId=e)},i=()=>{r()};return e.addEventListener(`scroll`,i,{passive:!0}),window.addEventListener(`resize`,i),typeof ResizeObserver<`u`&&(t.resizeObserver=new ResizeObserver(i),C(e)||t.resizeObserver.observe(e)),typeof IntersectionObserver<`u`&&(t.intersectionObserver=new IntersectionObserver(()=>{r()},{root:C(e)?null:e,rootMargin:`200% 200% 200% 200%`,threshold:0})),t.queueSample=r,t.cleanup=()=>{t.rafId!==0&&(cancelAnimationFrame(t.rafId),t.rafId=0),e.removeEventListener(`scroll`,i),window.removeEventListener(`resize`,i),t.resizeObserver?.disconnect(),t.intersectionObserver?.disconnect()},t}function k(e){let t=x.get(e);if(t)return t;let n=O(e);return x.set(e,n),n}function A(e,t){let n=k(e),r=!1;n.registrations.add(t),n.resizeObserver?.observe(t.node),n.intersectionObserver?.observe(t.node),n.queueSample();let i=()=>{r||(r=!0,n.registrations.delete(t),n.resizeObserver?.unobserve(t.node),n.intersectionObserver?.unobserve(t.node),t.dispose=void 0,n.registrations.size===0&&(n.cleanup(),x.delete(e)))};return t.dispose=i,i}function j(e,t){return t?t.current:e||(typeof window>`u`?null:window)}const M={display:`table`},N=({onEnter:t,onLeave:n,onEvent:i,children:a,once:s=!1,oncePerDirection:l=!1,fireOnInitialVisible:d=!1,disabled:f=!1,threshold:p=0,root:m=null,rootRef:h,rootMargin:g=`0px`,className:_})=>{let y=e.useRef(null),[b,x]=e.useState(null),S=e.useRef(null),C=e.useRef(null),w=e.useRef(null),T=e.useRef(null),D=u(g),O=v(p),k=a!=null,N=e.Children.count(a),P=N===1&&e.isValidElement(a)?a:null,F=c(k,N,P,P?P.type:null),I=F||!P?null:P,L=I?.props.ref,R=e.useCallback(e=>{if(o(L,e),e===null){S.current=null,x(e=>e===null?e:null);return}if(e instanceof Element){S.current=e,x(t=>t===e?t:e);return}S.current=null,x(e=>e===null?e:null),r(`[react-atom-trigger] Child mode requires the child ref to resolve to a DOM element. Observation is disabled for this render.`)},[L]);return e.useEffect(()=>{r(`[react-atom-trigger] v2 uses a new internal observation engine. If you upgraded from v1.x, verify trigger behavior for timing, threshold and rootMargin.`)},[]),e.useEffect(()=>{k&&_&&r("[react-atom-trigger] `className` only applies to the internal sentinel. In child mode, style the child element directly.")},[_,k]),e.useEffect(()=>{F&&r(F)},[F]),e.useEffect(()=>{if(typeof window>`u`||!k||!I||F||b)return;let e=window.setTimeout(()=>{S.current||r(`[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.`)},0);return()=>{window.clearTimeout(e)}},[b,I,k,F]),e.useEffect(()=>{let e=C.current;e&&(e.onEnter=t,e.onLeave=n,e.onEvent=i)},[t,n,i]),e.useEffect(()=>{if(typeof window>`u`)return;let e=k?b:y.current,r=j(m,h);if(!e){C.current&&E(C.current),w.current?.(),w.current=null,T.current=null;return}C.current?(C.current.node=e,C.current.rootMargin=D,C.current.threshold=O,C.current.once=s,C.current.oncePerDirection=l,C.current.fireOnInitialVisible=d):C.current={node:e,rootMargin:D,threshold:O,once:s,oncePerDirection:l,fireOnInitialVisible:d,onEnter:t,onLeave:n,onEvent:i,previousTriggerActive:void 0,previousRect:null,counts:{entered:0,left:0}};let a=C.current;if(f||!r){E(a),w.current?.(),w.current=null,T.current=null;return}let o=T.current,c={node:e,target:r,rootMargin:D,threshold:O,once:s,oncePerDirection:l,fireOnInitialVisible:d};(!o||o.node!==c.node||o.target!==c.target||o.rootMargin!==c.rootMargin||o.threshold!==c.threshold||o.once!==c.once||o.oncePerDirection!==c.oncePerDirection||o.fireOnInitialVisible!==c.fireOnInitialVisible)&&(E(a),w.current?.(),w.current=A(r,a),T.current=c)},[f,D,O,s,l,d,b,m,h?.current,k]),e.useEffect(()=>()=>{w.current?.(),w.current=null,T.current=null},[]),k?I?e.cloneElement(I,{ref:R}):e.createElement(e.Fragment,null,a):e.createElement(`div`,{ref:y,style:M,className:_})},P={x:0,y:0},F=typeof window>`u`?e.useEffect:e.useLayoutEffect;function I(e,t){let n=null,r=null,i=()=>{n&&(clearTimeout(n),n=null),r=Date.now(),e()};return{schedule:()=>{if(t<=0){i();return}let e=Date.now();if(r===null||e-r>=t){i();return}n||(n=setTimeout(()=>{i()},t-(e-r)))},cancel:()=>{n&&(clearTimeout(n),n=null)}}}function L(){return typeof window>`u`?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}function R(e){return!!(e&&typeof e==`object`&&!(typeof Window<`u`&&e instanceof Window)&&`current`in e)}function z(e){return R(e)?e.current:e||(typeof window>`u`?null:window)}function B(e){return e===window||typeof Window<`u`&&e instanceof Window}function V(e){if(B(e))return{x:e.scrollX,y:e.scrollY};let t=e;return{x:t.scrollLeft,y:t.scrollTop}}function H(t){let[n,r]=e.useState(L);return e.useEffect(()=>{if(typeof window>`u`||t?.enabled===!1)return;let e=t?.throttleMs??16;r(L());let n=I(()=>{r(L())},e);return window.addEventListener(`resize`,n.schedule,{passive:t?.passive}),()=>{n.cancel(),window.removeEventListener(`resize`,n.schedule)}},[t?.enabled,t?.passive,t?.throttleMs]),n}function U(t){let n=t?.target,r=R(n),[i,a]=e.useState(()=>{let e=z(n);return e?V(e):P}),[o,s]=e.useState(()=>r?z(n):null);F(()=>{if(!r)return;let e=z(n);s(t=>t===e?t:e)});let c=r?o:z(n);return e.useEffect(()=>{if(t?.enabled===!1){a(P);return}if(!c){a(P);return}let e=t?.throttleMs??16;a(V(c));let n=I(()=>{a(V(c))},e);return c.addEventListener(`scroll`,n.schedule,{passive:t?.passive}),()=>{n.cancel(),c.removeEventListener(`scroll`,n.schedule)}},[t?.enabled,t?.passive,t?.throttleMs,c]),i}export{N as AtomTrigger,U as useScrollPosition,H as useViewportSize};
@@ -0,0 +1 @@
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require(`react`)):typeof define==`function`&&define.amd?define([`exports`,`react`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.reactAtomTrigger={},e.React))})(this,function(e,t){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var n=Object.create,r=Object.defineProperty,i=Object.getOwnPropertyDescriptor,a=Object.getOwnPropertyNames,o=Object.getPrototypeOf,s=Object.prototype.hasOwnProperty,c=(e,t,n,o)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=a(t),l=0,u=c.length,d;l<u;l++)d=c[l],!s.call(e,d)&&d!==n&&r(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(o=i(t,d))||o.enumerable});return e};t=((e,t,i)=>(i=e==null?{}:n(o(e)),c(t||!e||!e.__esModule?r(i,`default`,{value:e,enumerable:!0}):i,e)))(t);let l=new Set;function u(){if(typeof process>`u`||!process.env)return null;let e=`production`;return e===`development`||e===`production`?e:null}function d(e){u()===`development`&&(l.has(e)||(l.add(e),typeof console<`u`&&console.warn&&console.warn(e)))}let f=Symbol.for(`react.forward_ref`),p=Symbol.for(`react.memo`);function m(e,t){if(e){if(typeof e==`function`){e(t);return}e.current=t}}function h(e){if(typeof e==`string`||typeof e==`function`)return!0;if(typeof e!=`object`||!e)return!1;let t=e;return t.$$typeof===f?!0:t.$$typeof===p&&t.type?h(t.type):!1}function g(e,n,r,i){return e?n===1?r?i===t.default.Fragment?`[react-atom-trigger] Child mode does not support React.Fragment. Wrap the content in a single DOM element. Observation is disabled for this render.`:(!i||h(i),null):`[react-atom-trigger] Child mode expects a React element child. Observation is disabled for this render.`:`[react-atom-trigger] Child mode expects exactly one top-level React element. Observation is disabled for this render.`:null}function _(e){return Array.isArray(e)&&e.length===4&&e.every(e=>typeof e==`number`&&Number.isFinite(e))}function v(e){return typeof e==`string`?e:_(e)?e.map(e=>`${Object.is(e,-0)?0:e}px`).join(` `):(e==null||d(`[react-atom-trigger] Invalid rootMargin array ${JSON.stringify(e)}. Use exactly four finite numbers: [top, right, bottom, left]. Falling back to 0px.`),`0px`)}function y(e,t){let n=e.trim();if(!n||/^[+-]?0(?:\.0+)?$/.test(n))return 0;let r=n.match(/^([+-]?(?:\d+\.?\d*|\.\d+))(px|%)$/);if(!r)return d(`[react-atom-trigger] Invalid rootMargin token "${n}". Use px, % or 0. Falling back to 0px.`),0;let[,i,a]=r,o=Number.parseFloat(i);return a===`%`?o/100*t:o}function b(e,t,n){let r=e.trim().split(/\s+/).filter(Boolean);if(r.length>4)return d(`[react-atom-trigger] Invalid rootMargin "${e}". Use 1 to 4 values in IntersectionObserver order. Falling back to 0px.`),{top:0,right:0,bottom:0,left:0};let[i=`0px`,a=i,o=i,s=a]=r;return{top:y(i,n),right:y(a,t),bottom:y(o,n),left:y(s,t)}}function x(e,t){let n=b(t,e.width,e.height);return new DOMRect(e.left-n.left,e.top-n.top,e.width+n.left+n.right,e.height+n.top+n.bottom)}function S(e){let t=e.width>0?e.width:1,n=e.height>0?e.height:1;return t===e.width&&n===e.height?e:new DOMRect(e.left,e.top,t,n)}function C(e,t){let n=Math.max(e.left,t.left),r=Math.max(e.top,t.top),i=Math.min(e.right,t.right),a=Math.min(e.bottom,t.bottom);return i<=n||a<=r?new DOMRect(0,0,0,0):new DOMRect(n,r,i-n,a-r)}function w(e,t){let n=e.width*e.height;return n<=0?0:t.width*t.height/n}function T(e){return Math.min(1,Math.max(0,e))}function E(e){return Array.isArray(e)?(d("[react-atom-trigger] `threshold` expects a single number in v2. Using the first finite numeric entry."),T(e.find(e=>typeof e==`number`&&Number.isFinite(e))??0)):e==null?0:typeof e!=`number`||!Number.isFinite(e)?(d("[react-atom-trigger] `threshold` must be a finite number between 0 and 1. Falling back to 0."),0):((e<0||e>1)&&d("[react-atom-trigger] `threshold` should be between 0 and 1. Values are clamped."),T(e))}function D(e,t){if(!e)return`unknown`;let n=t.left-e.left,r=t.top-e.top;return n===0&&r===0?`stationary`:Math.abs(r)>=Math.abs(n)?r<0?`up`:`down`:n<0?`left`:`right`}function O(e){if(e.isIntersecting)return`inside`;let t=e.boundingClientRect,n=e.rootBounds?.top??0,r=e.rootBounds?.bottom??window.innerHeight,i=e.rootBounds?.left??0,a=e.rootBounds?.right??window.innerWidth;return t.bottom<=n?`above`:t.top>=r?`below`:t.right<=i?`left`:t.left>=a?`right`:`outside`}let k=new WeakMap;function A(){return new DOMRect(0,0,window.innerWidth,window.innerHeight)}function j(e){return e===window||typeof Window<`u`&&e instanceof Window}function M(e,t,n,r){return!(n&&t.entered+t.left>0||r&&(e===`enter`&&t.entered>0||e===`leave`&&t.left>0))}function N(e){return e.once?e.counts.entered+e.counts.left>0:e.oncePerDirection?e.counts.entered>0&&e.counts.left>0:!1}function P(e){e.previousTriggerActive=void 0,e.previousRect=null}function F(e,t){let n=S(e.node.getBoundingClientRect()),r=x(t,e.rootMargin),i=C(n,r),a=w(n,i),o=a>0,s=e.previousTriggerActive,c=s===!0||e.threshold===0?o:a>=e.threshold,l=D(e.previousRect,n);e.previousRect=n,e.previousTriggerActive=c;let u=Date.now(),d={target:e.node,rootBounds:r,boundingClientRect:n,intersectionRect:i,isIntersecting:o,intersectionRatio:a,source:`geometry`},f=s===void 0;if(f&&(!e.fireOnInitialVisible||!c)||s===c)return;let p=c?`enter`:`leave`;if(!M(p,e.counts,e.once,e.oncePerDirection))return;let m=p===`enter`?{...e.counts,entered:e.counts.entered+1}:{...e.counts,left:e.counts.left+1};e.counts=m;let h={type:p,isInitial:f,entry:d,counts:m,movementDirection:l,position:O(d),timestamp:u};e.onEvent?.(h),p===`enter`?e.onEnter?.(h):e.onLeave?.(h),N(e)&&e.dispose?.()}function I(e){let t={registrations:new Set,rafId:0,resizeObserver:null,intersectionObserver:null,queueSample:()=>{},cleanup:()=>{}},n=()=>{if(t.rafId=0,t.registrations.size===0)return;let n=j(e)?A():e.getBoundingClientRect();for(let e of t.registrations)F(e,n)},r=()=>{if(t.rafId!==0)return;t.rafId=-1;let e=window.requestAnimationFrame(()=>{t.rafId=0,n()});t.rafId===-1&&(t.rafId=e)},i=()=>{r()};return e.addEventListener(`scroll`,i,{passive:!0}),window.addEventListener(`resize`,i),typeof ResizeObserver<`u`&&(t.resizeObserver=new ResizeObserver(i),j(e)||t.resizeObserver.observe(e)),typeof IntersectionObserver<`u`&&(t.intersectionObserver=new IntersectionObserver(()=>{r()},{root:j(e)?null:e,rootMargin:`200% 200% 200% 200%`,threshold:0})),t.queueSample=r,t.cleanup=()=>{t.rafId!==0&&(cancelAnimationFrame(t.rafId),t.rafId=0),e.removeEventListener(`scroll`,i),window.removeEventListener(`resize`,i),t.resizeObserver?.disconnect(),t.intersectionObserver?.disconnect()},t}function L(e){let t=k.get(e);if(t)return t;let n=I(e);return k.set(e,n),n}function R(e,t){let n=L(e),r=!1;n.registrations.add(t),n.resizeObserver?.observe(t.node),n.intersectionObserver?.observe(t.node),n.queueSample();let i=()=>{r||(r=!0,n.registrations.delete(t),n.resizeObserver?.unobserve(t.node),n.intersectionObserver?.unobserve(t.node),t.dispose=void 0,n.registrations.size===0&&(n.cleanup(),k.delete(e)))};return t.dispose=i,i}function z(e,t){return t?t.current:e||(typeof window>`u`?null:window)}let B={display:`table`},V=({onEnter:e,onLeave:n,onEvent:r,children:i,once:a=!1,oncePerDirection:o=!1,fireOnInitialVisible:s=!1,disabled:c=!1,threshold:l=0,root:u=null,rootRef:f,rootMargin:p=`0px`,className:h})=>{let _=t.default.useRef(null),[y,b]=t.default.useState(null),x=t.default.useRef(null),S=t.default.useRef(null),C=t.default.useRef(null),w=t.default.useRef(null),T=v(p),D=E(l),O=i!=null,k=t.default.Children.count(i),A=k===1&&t.default.isValidElement(i)?i:null,j=g(O,k,A,A?A.type:null),M=j||!A?null:A,N=M?.props.ref,F=t.default.useCallback(e=>{if(m(N,e),e===null){x.current=null,b(e=>e===null?e:null);return}if(e instanceof Element){x.current=e,b(t=>t===e?t:e);return}x.current=null,b(e=>e===null?e:null),d(`[react-atom-trigger] Child mode requires the child ref to resolve to a DOM element. Observation is disabled for this render.`)},[N]);return t.default.useEffect(()=>{d(`[react-atom-trigger] v2 uses a new internal observation engine. If you upgraded from v1.x, verify trigger behavior for timing, threshold and rootMargin.`)},[]),t.default.useEffect(()=>{O&&h&&d("[react-atom-trigger] `className` only applies to the internal sentinel. In child mode, style the child element directly.")},[h,O]),t.default.useEffect(()=>{j&&d(j)},[j]),t.default.useEffect(()=>{if(typeof window>`u`||!O||!M||j||y)return;let e=window.setTimeout(()=>{x.current||d(`[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.`)},0);return()=>{window.clearTimeout(e)}},[y,M,O,j]),t.default.useEffect(()=>{let t=S.current;t&&(t.onEnter=e,t.onLeave=n,t.onEvent=r)},[e,n,r]),t.default.useEffect(()=>{if(typeof window>`u`)return;let t=O?y:_.current,i=z(u,f);if(!t){S.current&&P(S.current),C.current?.(),C.current=null,w.current=null;return}S.current?(S.current.node=t,S.current.rootMargin=T,S.current.threshold=D,S.current.once=a,S.current.oncePerDirection=o,S.current.fireOnInitialVisible=s):S.current={node:t,rootMargin:T,threshold:D,once:a,oncePerDirection:o,fireOnInitialVisible:s,onEnter:e,onLeave:n,onEvent:r,previousTriggerActive:void 0,previousRect:null,counts:{entered:0,left:0}};let l=S.current;if(c||!i){P(l),C.current?.(),C.current=null,w.current=null;return}let d=w.current,p={node:t,target:i,rootMargin:T,threshold:D,once:a,oncePerDirection:o,fireOnInitialVisible:s};(!d||d.node!==p.node||d.target!==p.target||d.rootMargin!==p.rootMargin||d.threshold!==p.threshold||d.once!==p.once||d.oncePerDirection!==p.oncePerDirection||d.fireOnInitialVisible!==p.fireOnInitialVisible)&&(P(l),C.current?.(),C.current=R(i,l),w.current=p)},[c,T,D,a,o,s,y,u,f?.current,O]),t.default.useEffect(()=>()=>{C.current?.(),C.current=null,w.current=null},[]),O?M?t.default.cloneElement(M,{ref:F}):t.default.createElement(t.default.Fragment,null,i):t.default.createElement(`div`,{ref:_,style:B,className:h})},H={x:0,y:0},U=typeof window>`u`?t.default.useEffect:t.default.useLayoutEffect;function W(e,t){let n=null,r=null,i=()=>{n&&(clearTimeout(n),n=null),r=Date.now(),e()};return{schedule:()=>{if(t<=0){i();return}let e=Date.now();if(r===null||e-r>=t){i();return}n||(n=setTimeout(()=>{i()},t-(e-r)))},cancel:()=>{n&&(clearTimeout(n),n=null)}}}function G(){return typeof window>`u`?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}function K(e){return!!(e&&typeof e==`object`&&!(typeof Window<`u`&&e instanceof Window)&&`current`in e)}function q(e){return K(e)?e.current:e||(typeof window>`u`?null:window)}function J(e){return e===window||typeof Window<`u`&&e instanceof Window}function Y(e){if(J(e))return{x:e.scrollX,y:e.scrollY};let t=e;return{x:t.scrollLeft,y:t.scrollTop}}function X(e){let[n,r]=t.default.useState(G);return t.default.useEffect(()=>{if(typeof window>`u`||e?.enabled===!1)return;let t=e?.throttleMs??16;r(G());let n=W(()=>{r(G())},t);return window.addEventListener(`resize`,n.schedule,{passive:e?.passive}),()=>{n.cancel(),window.removeEventListener(`resize`,n.schedule)}},[e?.enabled,e?.passive,e?.throttleMs]),n}function Z(e){let n=e?.target,r=K(n),[i,a]=t.default.useState(()=>{let e=q(n);return e?Y(e):H}),[o,s]=t.default.useState(()=>r?q(n):null);U(()=>{if(!r)return;let e=q(n);s(t=>t===e?t:e)});let c=r?o:q(n);return t.default.useEffect(()=>{if(e?.enabled===!1){a(H);return}if(!c){a(H);return}let t=e?.throttleMs??16;a(Y(c));let n=W(()=>{a(Y(c))},t);return c.addEventListener(`scroll`,n.schedule,{passive:e?.passive}),()=>{n.cancel(),c.removeEventListener(`scroll`,n.schedule)}},[e?.enabled,e?.passive,e?.throttleMs,c]),i}e.AtomTrigger=V,e.useScrollPosition=Z,e.useViewportSize=X});
package/package.json CHANGED
@@ -1,47 +1,96 @@
1
1
  {
2
2
  "name": "react-atom-trigger",
3
- "type": "module",
4
- "description": "React component to execute code when you scroll to an element. Simple react-waypoint alternative in typescript.",
5
- "main": "lib/index.js",
6
- "module": "lib/index.es.js",
7
- "typings": "lib/index.d.ts",
8
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
+ "description": "Geometry-based scroll trigger for React with precise enter/leave control. A modern alternative to react-waypoint.",
5
+ "keywords": [
6
+ "intersection",
7
+ "observer",
8
+ "on-scroll",
9
+ "react",
10
+ "scroll",
11
+ "scroll-into-view",
12
+ "v2"
13
+ ],
14
+ "homepage": "https://atomtrigger.dev",
15
+ "bugs": {
16
+ "url": "https://github.com/innrvoice/react-atom-trigger/issues"
17
+ },
9
18
  "license": "MIT",
10
- "author": "innrVoice <innrvoice@icloud.com>",
19
+ "author": "Pavel Bochkov-Rastopchin <hello@visiofutura.com>",
11
20
  "repository": {
12
21
  "type": "git",
13
22
  "url": "git+https://github.com/innrvoice/react-atom-trigger.git"
14
23
  },
15
- "bugs": {
16
- "url": "https://github.com/innrvoice/react-atom-trigger/issues"
24
+ "files": [
25
+ "lib",
26
+ "MIGRATION.md"
27
+ ],
28
+ "type": "module",
29
+ "main": "./lib/index.js",
30
+ "module": "./lib/index.js",
31
+ "types": "./lib/index.d.ts",
32
+ "typings": "./lib/index.d.ts",
33
+ "unpkg": "./lib/index.umd.js",
34
+ "jsdelivr": "./lib/index.umd.js",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./lib/index.d.ts",
38
+ "import": "./lib/index.js",
39
+ "default": "./lib/index.js"
40
+ }
17
41
  },
18
42
  "scripts": {
19
43
  "build": "tsdown",
20
- "lint": "oxlint . --ignore-pattern node_modules --ignore-pattern lib"
21
- },
22
- "peerDependencies": {
23
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
44
+ "build:sb": "pnpm build-storybook",
45
+ "deploy:storybook": "pnpm build:sb",
46
+ "format": "oxfmt --write .",
47
+ "format:check": "oxfmt --check .",
48
+ "lint": "oxlint . --ignore-pattern node_modules --ignore-pattern lib",
49
+ "prepare": "node scripts/setup-git-hooks.mjs",
50
+ "preview:sb": "pnpm build:sb && npx serve -s storybook-static -l 3000",
51
+ "precommit:checks": "pnpm format:check && pnpm lint && pnpm test",
52
+ "test": "pnpm test:all",
53
+ "test:all": "pnpm test:unit && pnpm test:storybook",
54
+ "test:unit": "vitest run --project=unit --reporter=verbose",
55
+ "test:watch": "vitest --project=unit",
56
+ "test:storybook": "vitest run --project=storybook",
57
+ "storybook": "storybook dev",
58
+ "build-storybook": "storybook build -o storybook-static"
24
59
  },
25
60
  "devDependencies": {
61
+ "@storybook/addon-docs": "10.3.4",
62
+ "@storybook/addon-vitest": "10.3.4",
63
+ "@storybook/react-vite": "10.3.4",
64
+ "@testing-library/dom": "^10.4.1",
65
+ "@testing-library/react": "^16.3.2",
26
66
  "@types/node": "^20.6.3",
27
67
  "@types/react": "^19.0.12",
68
+ "@types/react-dom": "^19.2.3",
69
+ "@vitest/browser-playwright": "4.1.2",
70
+ "@vitest/coverage-v8": "4.1.2",
71
+ "jsdom": "^29.0.1",
72
+ "oxfmt": "^0.43.0",
28
73
  "oxlint": "^1.50.0",
29
- "prettier": "^3.4.2",
30
- "tsdown": "^0.21.0-beta.2",
74
+ "playwright": "^1.59.1",
75
+ "react": "^19.2.4",
76
+ "react-dom": "^19.2.4",
77
+ "storybook": "10.3.4",
78
+ "tsdown": "^0.21.7",
31
79
  "tslib": "^2.8.0",
32
- "typescript": "^5.6.3"
80
+ "typescript": "^5.6.3",
81
+ "vite": "^6.0.0",
82
+ "vitest": "^4.1.2"
83
+ },
84
+ "peerDependencies": {
85
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
33
86
  },
34
- "keywords": [
35
- "react",
36
- "scroll",
37
- "on-scroll",
38
- "scroll-into-view",
39
- "waypoint-alternative"
40
- ],
41
- "files": [
42
- "lib"
43
- ],
44
87
  "engines": {
45
88
  "node": ">=20.19.0"
89
+ },
90
+ "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
91
+ "pnpm": {
92
+ "overrides": {
93
+ "picomatch": "^4.0.4"
94
+ }
46
95
  }
47
96
  }
package/lib/index.es.js DELETED
@@ -1 +0,0 @@
1
- import e from"react";const t=({scrollEvent:t,callback:n,getDebugInfo:r,triggerOnce:i=!1,className:a,behavior:o=`default`,dimensions:s,offset:c=[0,0,0,0]})=>{let l=e.useRef(null),[u,d]=e.useState(void 0),f=e.useRef(void 0),[p,m]=e.useState({leftViewport:0,enteredViewport:0});return e.useLayoutEffect(()=>{if(l.current){let e=l.current.getBoundingClientRect(),[t,n,r,i]=c;e.top>t&&e.bottom<s.height-r&&e.left>i&&e.right<s.width-n?d(`inViewport`):e.top>s.height-r?d(`bottom`):d(`top`)}},[l,t,s,c]),e.useLayoutEffect(()=>{if(f.current===void 0&&u!==void 0&&(f.current=u),u===`inViewport`&&(f.current===`bottom`||f.current===`top`)){if(o===`enter`&&(!i||i&&p.enteredViewport)||o===`default`&&(!i||i&&(p.enteredViewport<1||p.leftViewport<1))){n&&n();let e={...p,enteredViewport:p.enteredViewport+1};r&&r({timesTriggered:e,trigger:`entered`}),m(e)}f.current=u}if((u===`top`||u===`bottom`)&&f.current===`inViewport`&&(f.current=u,o===`leave`&&(!i||i&&p.leftViewport===0)||o===`default`&&(!i||i&&(p.leftViewport<1||p.enteredViewport<1)))){n&&n();let e={...p,leftViewport:p.leftViewport+1};r&&r({timesTriggered:e,trigger:`left`}),m(e)}},[u,n,i,o,r]),e.createElement(`div`,{ref:l,style:{display:`table`},className:a})};function n(e,t){}function r(){let{scrollX:e,scrollY:t}=window;return{scrollX:e,scrollY:t}}function i(){let{innerWidth:e,innerHeight:t}=window;return{width:e,height:t}}function a(t){let[n,r]=e.useState(i()),a=e.useRef(null),o=e.useRef(!1),s=t?.eventListenerTimeoutMs||15;return e.useEffect(()=>{r(i());function e(){a.current&&clearTimeout(a.current),a.current=setTimeout(()=>r(i()),s)}return window.addEventListener(`resize`,e,{passive:t?.passiveEventListener}),o.current=!0,()=>{o&&window.removeEventListener(`resize`,e)}},[]),n}function o({containerRef:t,options:n}){let[i,a]=e.useState(r()),o=e.useRef(null),s=e.useRef(!1);return e.useEffect(()=>{let e=e=>{let t=e.target;o.current&&clearTimeout(o.current),o.current=setTimeout(()=>{a({scrollX:t.scrollLeft,scrollY:t.scrollTop})},n?.eventListenerTimeoutMs||15)},r=t?.current;return r&&(r&&s.current===!1&&r.addEventListener(`scroll`,e,{passive:n?.passiveEventListener}),s.current=!0),()=>{s&&r&&r.removeEventListener(`scroll`,e)}},[t]),i}function s(t){let[n,i]=e.useState(r()),a=e.useRef(null),o=e.useRef(!1);return e.useEffect(()=>{let e=()=>{a.current&&clearTimeout(a.current),a.current=setTimeout(()=>{let{scrollX:e,scrollY:t}=r();i({scrollX:e,scrollY:t})},t?.eventListenerTimeoutMs||20)};return window.addEventListener(`scroll`,e,{passive:t?.passiveEventListener}),o.current=!0,()=>{o&&window.removeEventListener(`scroll`,e)}},[]),n}export{t as AtomTrigger,n as log,o as useContainerScroll,a as useWindowDimensions,s as useWindowScroll};