react-native-puff-pop 1.0.3 → 1.0.4
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/README.md +19 -0
- package/lib/module/index.js +28 -6
- package/lib/typescript/src/index.d.ts +15 -1
- package/package.json +1 -1
- package/src/index.tsx +60 -4
package/README.md
CHANGED
|
@@ -164,10 +164,13 @@ function App() {
|
|
|
164
164
|
| `skeleton` | `boolean` | `true` | Reserve space before animation |
|
|
165
165
|
| `visible` | `boolean` | `true` | Control visibility |
|
|
166
166
|
| `animateOnMount` | `boolean` | `true` | Animate when component mounts |
|
|
167
|
+
| `onAnimationStart` | `() => void` | - | Callback when animation starts |
|
|
167
168
|
| `onAnimationComplete` | `() => void` | - | Callback when animation completes |
|
|
168
169
|
| `style` | `ViewStyle` | - | Custom container style |
|
|
169
170
|
| `loop` | `boolean \| number` | `false` | Loop animation (true=infinite, number=times) |
|
|
170
171
|
| `loopDelay` | `number` | `0` | Delay between loop iterations in ms |
|
|
172
|
+
| `respectReduceMotion` | `boolean` | `true` | Respect system reduce motion setting |
|
|
173
|
+
| `testID` | `string` | - | Test ID for testing purposes |
|
|
171
174
|
|
|
172
175
|
### Animation Effects (`PuffPopEffect`)
|
|
173
176
|
|
|
@@ -204,6 +207,22 @@ The component reserves its full space immediately, and only the visual appearanc
|
|
|
204
207
|
### `skeleton={false}`
|
|
205
208
|
The component's height starts at 0 and expands during animation, pushing other content below it. This creates a more dynamic entrance effect.
|
|
206
209
|
|
|
210
|
+
## Accessibility
|
|
211
|
+
|
|
212
|
+
PuffPop respects the system's "Reduce Motion" accessibility setting by default. When users have enabled reduce motion in their device settings, animations will be instant (0 duration) to avoid discomfort.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// Respect reduce motion setting (default)
|
|
216
|
+
<PuffPop respectReduceMotion={true}>
|
|
217
|
+
<YourComponent />
|
|
218
|
+
</PuffPop>
|
|
219
|
+
|
|
220
|
+
// Ignore reduce motion setting (always animate)
|
|
221
|
+
<PuffPop respectReduceMotion={false}>
|
|
222
|
+
<YourComponent />
|
|
223
|
+
</PuffPop>
|
|
224
|
+
```
|
|
225
|
+
|
|
207
226
|
## License
|
|
208
227
|
|
|
209
228
|
MIT
|
package/lib/module/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState, useCallback, useMemo, } from 'react';
|
|
3
|
-
import { View, Animated, StyleSheet, Easing, } from 'react-native';
|
|
3
|
+
import { View, Animated, StyleSheet, Easing, AccessibilityInfo, } from 'react-native';
|
|
4
4
|
/**
|
|
5
5
|
* Get easing function based on type
|
|
6
6
|
*/
|
|
@@ -25,7 +25,7 @@ function getEasing(type) {
|
|
|
25
25
|
/**
|
|
26
26
|
* PuffPop - Animate children with beautiful entrance effects
|
|
27
27
|
*/
|
|
28
|
-
export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0, easing = 'easeOut', skeleton = true, visible = true, onAnimationComplete, style, animateOnMount = true, loop = false, loopDelay = 0, }) {
|
|
28
|
+
export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0, easing = 'easeOut', skeleton = true, visible = true, onAnimationComplete, onAnimationStart, style, animateOnMount = true, loop = false, loopDelay = 0, respectReduceMotion = true, testID, }) {
|
|
29
29
|
// Animation values
|
|
30
30
|
const opacity = useRef(new Animated.Value(animateOnMount ? 0 : 1)).current;
|
|
31
31
|
const scale = useRef(new Animated.Value(animateOnMount ? getInitialScale(effect) : 1)).current;
|
|
@@ -38,6 +38,23 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
38
38
|
const hasAnimated = useRef(false);
|
|
39
39
|
const loopAnimationRef = useRef(null);
|
|
40
40
|
const loopTimeoutRef = useRef(null);
|
|
41
|
+
// Reduce motion accessibility support
|
|
42
|
+
const [isReduceMotionEnabled, setIsReduceMotionEnabled] = useState(false);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!respectReduceMotion)
|
|
45
|
+
return;
|
|
46
|
+
const checkReduceMotion = async () => {
|
|
47
|
+
const reduceMotion = await AccessibilityInfo.isReduceMotionEnabled();
|
|
48
|
+
setIsReduceMotionEnabled(reduceMotion);
|
|
49
|
+
};
|
|
50
|
+
checkReduceMotion();
|
|
51
|
+
const subscription = AccessibilityInfo.addEventListener('reduceMotionChanged', setIsReduceMotionEnabled);
|
|
52
|
+
return () => {
|
|
53
|
+
subscription.remove();
|
|
54
|
+
};
|
|
55
|
+
}, [respectReduceMotion]);
|
|
56
|
+
// Effective duration (0 if reduce motion is enabled)
|
|
57
|
+
const effectiveDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : duration;
|
|
41
58
|
// Memoize effect type checks to avoid repeated includes() calls
|
|
42
59
|
const effectFlags = useMemo(() => ({
|
|
43
60
|
hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect),
|
|
@@ -65,12 +82,16 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
65
82
|
}, [skeleton, measuredHeight]);
|
|
66
83
|
// Animate function
|
|
67
84
|
const animate = useCallback((toVisible) => {
|
|
85
|
+
// Call onAnimationStart callback
|
|
86
|
+
if (toVisible && onAnimationStart) {
|
|
87
|
+
onAnimationStart();
|
|
88
|
+
}
|
|
68
89
|
const easingFn = getEasing(easing);
|
|
69
90
|
// When skeleton is false, we animate height which doesn't support native driver
|
|
70
91
|
// So we must use JS driver for all animations in that case
|
|
71
92
|
const useNative = skeleton;
|
|
72
93
|
const config = {
|
|
73
|
-
duration,
|
|
94
|
+
duration: effectiveDuration,
|
|
74
95
|
easing: easingFn,
|
|
75
96
|
useNativeDriver: useNative,
|
|
76
97
|
};
|
|
@@ -118,7 +139,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
118
139
|
const targetHeight = toVisible ? measuredHeight : 0;
|
|
119
140
|
animations.push(Animated.timing(animatedHeight, {
|
|
120
141
|
toValue: targetHeight,
|
|
121
|
-
duration,
|
|
142
|
+
duration: effectiveDuration,
|
|
122
143
|
easing: easingFn,
|
|
123
144
|
useNativeDriver: false,
|
|
124
145
|
}));
|
|
@@ -196,12 +217,13 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
196
217
|
}
|
|
197
218
|
}, [
|
|
198
219
|
delay,
|
|
199
|
-
|
|
220
|
+
effectiveDuration,
|
|
200
221
|
easing,
|
|
201
222
|
effect,
|
|
202
223
|
effectFlags,
|
|
203
224
|
measuredHeight,
|
|
204
225
|
onAnimationComplete,
|
|
226
|
+
onAnimationStart,
|
|
205
227
|
opacity,
|
|
206
228
|
rotate,
|
|
207
229
|
scale,
|
|
@@ -277,7 +299,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
277
299
|
if (!skeleton && measuredHeight === null) {
|
|
278
300
|
return (_jsx(View, { style: styles.measureContainer, onLayout: onLayout, children: _jsx(View, { style: styles.hidden, children: children }) }));
|
|
279
301
|
}
|
|
280
|
-
return (_jsx(Animated.View, { style: [styles.container, style, containerAnimatedStyle], children: _jsx(Animated.View, { style: animatedStyle, children: children }) }));
|
|
302
|
+
return (_jsx(Animated.View, { style: [styles.container, style, containerAnimatedStyle], testID: testID, children: _jsx(Animated.View, { style: animatedStyle, children: children }) }));
|
|
281
303
|
}
|
|
282
304
|
/**
|
|
283
305
|
* Get initial scale value based on effect
|
|
@@ -69,9 +69,23 @@ export interface PuffPopProps {
|
|
|
69
69
|
* @default 0
|
|
70
70
|
*/
|
|
71
71
|
loopDelay?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Callback when animation starts
|
|
74
|
+
*/
|
|
75
|
+
onAnimationStart?: () => void;
|
|
76
|
+
/**
|
|
77
|
+
* Respect system reduce motion accessibility setting
|
|
78
|
+
* When true and reduce motion is enabled, animations will be instant
|
|
79
|
+
* @default true
|
|
80
|
+
*/
|
|
81
|
+
respectReduceMotion?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Test ID for testing purposes
|
|
84
|
+
*/
|
|
85
|
+
testID?: string;
|
|
72
86
|
}
|
|
73
87
|
/**
|
|
74
88
|
* PuffPop - Animate children with beautiful entrance effects
|
|
75
89
|
*/
|
|
76
|
-
export declare function PuffPop({ children, effect, duration, delay, easing, skeleton, visible, onAnimationComplete, style, animateOnMount, loop, loopDelay, }: PuffPopProps): ReactElement;
|
|
90
|
+
export declare function PuffPop({ children, effect, duration, delay, easing, skeleton, visible, onAnimationComplete, onAnimationStart, style, animateOnMount, loop, loopDelay, respectReduceMotion, testID, }: PuffPopProps): ReactElement;
|
|
77
91
|
export default PuffPop;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-puff-pop",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A React Native animation library for revealing children components with beautiful puff and pop effects",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
package/src/index.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Animated,
|
|
13
13
|
StyleSheet,
|
|
14
14
|
Easing,
|
|
15
|
+
AccessibilityInfo,
|
|
15
16
|
type LayoutChangeEvent,
|
|
16
17
|
type StyleProp,
|
|
17
18
|
type ViewStyle,
|
|
@@ -116,6 +117,23 @@ export interface PuffPopProps {
|
|
|
116
117
|
* @default 0
|
|
117
118
|
*/
|
|
118
119
|
loopDelay?: number;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Callback when animation starts
|
|
123
|
+
*/
|
|
124
|
+
onAnimationStart?: () => void;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Respect system reduce motion accessibility setting
|
|
128
|
+
* When true and reduce motion is enabled, animations will be instant
|
|
129
|
+
* @default true
|
|
130
|
+
*/
|
|
131
|
+
respectReduceMotion?: boolean;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Test ID for testing purposes
|
|
135
|
+
*/
|
|
136
|
+
testID?: string;
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
/**
|
|
@@ -152,10 +170,13 @@ export function PuffPop({
|
|
|
152
170
|
skeleton = true,
|
|
153
171
|
visible = true,
|
|
154
172
|
onAnimationComplete,
|
|
173
|
+
onAnimationStart,
|
|
155
174
|
style,
|
|
156
175
|
animateOnMount = true,
|
|
157
176
|
loop = false,
|
|
158
177
|
loopDelay = 0,
|
|
178
|
+
respectReduceMotion = true,
|
|
179
|
+
testID,
|
|
159
180
|
}: PuffPopProps): ReactElement {
|
|
160
181
|
// Animation values
|
|
161
182
|
const opacity = useRef(new Animated.Value(animateOnMount ? 0 : 1)).current;
|
|
@@ -171,6 +192,32 @@ export function PuffPop({
|
|
|
171
192
|
const loopAnimationRef = useRef<Animated.CompositeAnimation | null>(null);
|
|
172
193
|
const loopTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
173
194
|
|
|
195
|
+
// Reduce motion accessibility support
|
|
196
|
+
const [isReduceMotionEnabled, setIsReduceMotionEnabled] = useState(false);
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (!respectReduceMotion) return;
|
|
200
|
+
|
|
201
|
+
const checkReduceMotion = async () => {
|
|
202
|
+
const reduceMotion = await AccessibilityInfo.isReduceMotionEnabled();
|
|
203
|
+
setIsReduceMotionEnabled(reduceMotion);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
checkReduceMotion();
|
|
207
|
+
|
|
208
|
+
const subscription = AccessibilityInfo.addEventListener(
|
|
209
|
+
'reduceMotionChanged',
|
|
210
|
+
setIsReduceMotionEnabled
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return () => {
|
|
214
|
+
subscription.remove();
|
|
215
|
+
};
|
|
216
|
+
}, [respectReduceMotion]);
|
|
217
|
+
|
|
218
|
+
// Effective duration (0 if reduce motion is enabled)
|
|
219
|
+
const effectiveDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : duration;
|
|
220
|
+
|
|
174
221
|
// Memoize effect type checks to avoid repeated includes() calls
|
|
175
222
|
const effectFlags = useMemo(() => ({
|
|
176
223
|
hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect),
|
|
@@ -208,12 +255,17 @@ export function PuffPop({
|
|
|
208
255
|
// Animate function
|
|
209
256
|
const animate = useCallback(
|
|
210
257
|
(toVisible: boolean) => {
|
|
258
|
+
// Call onAnimationStart callback
|
|
259
|
+
if (toVisible && onAnimationStart) {
|
|
260
|
+
onAnimationStart();
|
|
261
|
+
}
|
|
262
|
+
|
|
211
263
|
const easingFn = getEasing(easing);
|
|
212
264
|
// When skeleton is false, we animate height which doesn't support native driver
|
|
213
265
|
// So we must use JS driver for all animations in that case
|
|
214
266
|
const useNative = skeleton;
|
|
215
267
|
const config = {
|
|
216
|
-
duration,
|
|
268
|
+
duration: effectiveDuration,
|
|
217
269
|
easing: easingFn,
|
|
218
270
|
useNativeDriver: useNative,
|
|
219
271
|
};
|
|
@@ -279,7 +331,7 @@ export function PuffPop({
|
|
|
279
331
|
animations.push(
|
|
280
332
|
Animated.timing(animatedHeight, {
|
|
281
333
|
toValue: targetHeight,
|
|
282
|
-
duration,
|
|
334
|
+
duration: effectiveDuration,
|
|
283
335
|
easing: easingFn,
|
|
284
336
|
useNativeDriver: false,
|
|
285
337
|
})
|
|
@@ -364,12 +416,13 @@ export function PuffPop({
|
|
|
364
416
|
},
|
|
365
417
|
[
|
|
366
418
|
delay,
|
|
367
|
-
|
|
419
|
+
effectiveDuration,
|
|
368
420
|
easing,
|
|
369
421
|
effect,
|
|
370
422
|
effectFlags,
|
|
371
423
|
measuredHeight,
|
|
372
424
|
onAnimationComplete,
|
|
425
|
+
onAnimationStart,
|
|
373
426
|
opacity,
|
|
374
427
|
rotate,
|
|
375
428
|
scale,
|
|
@@ -465,7 +518,10 @@ export function PuffPop({
|
|
|
465
518
|
}
|
|
466
519
|
|
|
467
520
|
return (
|
|
468
|
-
<Animated.View
|
|
521
|
+
<Animated.View
|
|
522
|
+
style={[styles.container, style, containerAnimatedStyle]}
|
|
523
|
+
testID={testID}
|
|
524
|
+
>
|
|
469
525
|
<Animated.View style={animatedStyle}>{children}</Animated.View>
|
|
470
526
|
</Animated.View>
|
|
471
527
|
);
|