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 +3 -3
- package/MIGRATION.md +255 -0
- package/README.md +257 -39
- package/lib/index.d.ts +55 -35
- package/lib/index.js +1 -1
- package/lib/index.umd.js +1 -0
- package/package.json +75 -26
- package/lib/index.es.js +0 -1
package/LICENSE
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2022
|
|
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
|
|
9
|
-
copies of the Software
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
76
|
+
If you want an already-visible trigger to behave like a normal first `enter`, pass
|
|
77
|
+
`fireOnInitialVisible`.
|
|
24
78
|
|
|
25
|
-
|
|
79
|
+
```tsx
|
|
80
|
+
import React from 'react';
|
|
81
|
+
import { AtomTrigger } from 'react-atom-trigger';
|
|
26
82
|
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
218
|
+
Both hooks are SSR-safe. Default throttling is `16ms`.
|
|
59
219
|
|
|
60
|
-
##
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
242
|
+
This package is built with `tsdown`.
|
|
67
243
|
|
|
68
244
|
```text
|
|
69
245
|
lib/index.js
|
|
70
|
-
lib/index.
|
|
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
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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<
|
|
47
|
+
declare const AtomTrigger: React.FC<AtomTriggerProps>;
|
|
32
48
|
//#endregion
|
|
33
49
|
//#region src/utils.d.ts
|
|
34
|
-
type
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
39
|
-
declare function
|
|
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,
|
|
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};
|
package/lib/index.umd.js
ADDED
|
@@ -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
|
-
"
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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": "
|
|
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
|
-
"
|
|
16
|
-
"
|
|
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
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
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
|
-
"
|
|
30
|
-
"
|
|
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};
|