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 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
@@ -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
- duration,
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",
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
- duration,
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 style={[styles.container, style, containerAnimatedStyle]}>
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
  );