@baby-journey/rn-segmented-progress-bar 0.1.5 → 0.4.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.
Files changed (50) hide show
  1. package/lib/commonjs/helpers/index.js +4 -5
  2. package/lib/commonjs/helpers/index.js.map +1 -1
  3. package/lib/commonjs/index.js +198 -142
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/package.json +1 -0
  6. package/lib/module/helpers/index.js +6 -5
  7. package/lib/module/helpers/index.js.map +1 -1
  8. package/lib/module/index.js +199 -139
  9. package/lib/module/index.js.map +1 -1
  10. package/lib/typescript/helpers/index.d.ts +2 -2
  11. package/lib/typescript/helpers/index.d.ts.map +1 -1
  12. package/lib/typescript/index.d.ts +1 -2
  13. package/lib/typescript/index.d.ts.map +1 -1
  14. package/package.json +23 -16
  15. package/src/helpers/index.ts +16 -9
  16. package/src/index.tsx +236 -190
  17. package/.editorconfig +0 -15
  18. package/.gitattributes +0 -3
  19. package/.gitignore +0 -70
  20. package/.nvmrc +0 -1
  21. package/.watchmanconfig +0 -1
  22. package/.yarnrc +0 -3
  23. package/CODE_OF_CONDUCT.md +0 -133
  24. package/CONTRIBUTING.md +0 -116
  25. package/babel.config.js +0 -3
  26. package/docs/CODE_OF_CONDUCT.md +0 -44
  27. package/docs/CONTRIBUTING.md +0 -95
  28. package/docs/pull_request_template.md +0 -28
  29. package/example/.expo/README.md +0 -8
  30. package/example/.expo/devices.json +0 -3
  31. package/example/App.js +0 -1
  32. package/example/app.json +0 -33
  33. package/example/assets/adaptive-icon.png +0 -0
  34. package/example/assets/favicon.png +0 -0
  35. package/example/assets/icon.png +0 -0
  36. package/example/assets/splash.png +0 -0
  37. package/example/babel.config.js +0 -22
  38. package/example/metro.config.js +0 -38
  39. package/example/package.json +0 -30
  40. package/example/src/App.tsx +0 -41
  41. package/example/tsconfig.json +0 -6
  42. package/example/webpack.config.js +0 -25
  43. package/example/yarn.lock +0 -10640
  44. package/lefthook.yml +0 -16
  45. package/scripts/bootstrap.js +0 -29
  46. package/src/__tests__/helper/index.test.js +0 -92
  47. package/src/__tests__/index.test.js +0 -28
  48. package/tsconfig.build.json +0 -5
  49. package/tsconfig.json +0 -28
  50. package/yarn.lock +0 -9933
package/src/index.tsx CHANGED
@@ -1,12 +1,13 @@
1
- import React, {
1
+ import {
2
2
  forwardRef,
3
- ForwardRefRenderFunction,
3
+ type ForwardRefRenderFunction,
4
4
  memo,
5
5
  useCallback,
6
6
  useEffect,
7
7
  useImperativeHandle,
8
8
  useMemo,
9
9
  useRef,
10
+ useState,
10
11
  } from 'react';
11
12
  import { Animated, Easing, StyleSheet, View } from 'react-native';
12
13
  import Svg, { Circle, G, TSpan } from 'react-native-svg';
@@ -39,6 +40,50 @@ const ProgressCircle = Animated.createAnimatedComponent(Circle);
39
40
 
40
41
  const max = 100;
41
42
  const duration = 1200;
43
+ const progressDelay = 500;
44
+
45
+ const INDICATOR_FONT = {
46
+ textAnchor: 'middle' as const,
47
+ fontSize: 18,
48
+ };
49
+
50
+ /**
51
+ * Computes the declarative strokeDashoffset for a segment.
52
+ * Returns an Animated.AnimatedInterpolation when the segment has progress,
53
+ * or a static number when it doesn't.
54
+ */
55
+ const computeStrokeDashoffset = (
56
+ animatedVal: Animated.Value,
57
+ target: number,
58
+ circumference: number,
59
+ numSegments: number,
60
+ gap: number
61
+ ): Animated.AnimatedInterpolation<number> | number => {
62
+ if (target <= 0) return circumference;
63
+
64
+ const gapAdjust = (numSegments * target * gap) / 100;
65
+ const targetOffset = circumference * (1 - target / 100) + gapAdjust;
66
+ const thresholdV = gapAdjust > 0 ? (gapAdjust * 100) / circumference : 0;
67
+
68
+ // Gap consumes all progress — segment stays hidden
69
+ if (thresholdV >= target) return circumference;
70
+
71
+ // With gap: dead zone at start where offset stays at circumference
72
+ if (thresholdV > 0) {
73
+ return animatedVal.interpolate({
74
+ inputRange: [0, thresholdV, target],
75
+ outputRange: [circumference, circumference, targetOffset],
76
+ extrapolate: 'clamp',
77
+ });
78
+ }
79
+
80
+ // No gap: simple linear interpolation
81
+ return animatedVal.interpolate({
82
+ inputRange: [0, target],
83
+ outputRange: [circumference, targetOffset],
84
+ extrapolate: 'clamp',
85
+ });
86
+ };
42
87
 
43
88
  const RNSegmentedProgressBar: ForwardRefRenderFunction<
44
89
  RunAnimationHandler,
@@ -55,15 +100,26 @@ const RNSegmentedProgressBar: ForwardRefRenderFunction<
55
100
  centerComponent,
56
101
  } = props;
57
102
 
58
- const circleRef = useRef([]);
59
-
60
103
  const animatedValue = useRef(new Animated.Value(0)).current;
61
104
  const progressAnimatedValues = useRef(
62
- [...Array(segments)].map(() => new Animated.Value(0))
105
+ new Array(segments).fill(null).map(() => new Animated.Value(0))
63
106
  ).current;
64
107
 
65
- const indicatorCircleRef = useRef(null);
66
- const tSpanRef = useRef(null);
108
+ const indicatorCircleRef = useRef<any>(null);
109
+
110
+ const tSpanRef = useRef<any>(null);
111
+
112
+ // Per-segment target values — drives both render-time interpolation and animation
113
+ const [segmentTargets, setSegmentTargets] = useState<number[]>(() =>
114
+ new Array(segments).fill(0)
115
+ );
116
+ // Overall progress stored in ref (only needed inside animation, not for render)
117
+ const activeProgressRef = useRef(0);
118
+ // Pending animation config — bridges run() and the post-render effect
119
+ const pendingAnimationRef = useRef<{
120
+ progress: number;
121
+ targets: number[];
122
+ } | null>(null);
67
123
 
68
124
  const indicatorSegmentsGap = indicator?.radius ?? 0;
69
125
  const halfCircle = radius + strokeWidth + indicatorSegmentsGap;
@@ -71,214 +127,224 @@ const RNSegmentedProgressBar: ForwardRefRenderFunction<
71
127
  const rotation = -90 + (180 * (segmentsGap / 2 / radius)) / Math.PI;
72
128
 
73
129
  const getProgressValues = useCallback(
74
- (progress) => getPathValues(progress, max, segments),
130
+ (progress: number) => getPathValues(progress, max, segments),
75
131
  [segments]
76
132
  );
77
133
 
78
- const progressDelay = 10;
79
-
80
- const animation = useCallback(
81
- (
82
- animatedVal: Animated.Value,
83
- toValue: number,
84
- delay: number,
85
- durationValue: number
86
- ) => {
87
- return Animated.timing(animatedVal, {
88
- toValue,
89
- duration: durationValue,
90
- delay,
91
- useNativeDriver: true,
92
- easing: Easing.linear,
93
- });
94
- },
95
- []
96
- );
97
-
98
- useEffect(() => {
99
- () => {
100
- animatedValue.removeAllListeners();
101
- progressAnimatedValues.forEach((progressAnimatedValue) =>
102
- progressAnimatedValue.removeAllListeners()
103
- );
104
- };
105
- }, [animatedValue, progressAnimatedValues]);
106
-
107
134
  const getMeanSegmentsGap = useCallback(
108
135
  (progress: number) => {
109
136
  const pathValues = getProgressValues(progress);
137
+ const activeSegments = pathValues.filter((val) => val > 0).length;
110
138
  return (
111
- ((progress / pathValues.filter((val) => val > 0).length || 1) *
112
- segments *
113
- segmentsGap) /
114
- 100
139
+ ((progress / (activeSegments || 1)) * segments * segmentsGap) / 100
115
140
  );
116
141
  },
117
142
  [getProgressValues, segments, segmentsGap]
118
143
  );
119
144
 
120
- const runIndicator = useCallback(
121
- (calculatedStrokeDashoffset: number, val: number) => {
122
- const { x: cx, y: cy } = getArcEndCoordinates(
123
- radius,
124
- calculatedStrokeDashoffset,
125
- halfCircle,
126
- halfCircle,
127
- rotation
128
- );
145
+ // Clean up on unmount
146
+ useEffect(() => {
147
+ return () => {
148
+ animatedValue.stopAnimation();
149
+ animatedValue.removeAllListeners();
150
+ progressAnimatedValues.forEach((v) => {
151
+ v.stopAnimation();
152
+ v.removeAllListeners();
153
+ });
154
+ };
155
+ }, [animatedValue, progressAnimatedValues]);
129
156
 
130
- if (!calculatedStrokeDashoffset) {
131
- return;
132
- }
157
+ // Start animations AFTER React has re-rendered with updated segmentTargets.
158
+ // This guarantees progressTrack useMemo has correct interpolations before
159
+ // any animated values start ticking.
160
+ useEffect(() => {
161
+ const pending = pendingAnimationRef.current;
162
+ if (!pending) return;
163
+ pendingAnimationRef.current = null;
164
+
165
+ const { progress, targets } = pending;
166
+
167
+ const progressAnimations = Animated.sequence(
168
+ progressAnimatedValues.map((animVal, index) =>
169
+ Animated.timing(animVal, {
170
+ toValue: targets[index] ?? 0,
171
+ duration: (duration * (targets[index] ?? 0)) / max,
172
+ delay: index === 0 ? progressDelay : 0,
173
+ useNativeDriver: false,
174
+ easing: Easing.linear,
175
+ })
176
+ )
177
+ );
178
+
179
+ if (indicator?.show) {
180
+ const percentageAnim = Animated.timing(animatedValue, {
181
+ toValue: progress,
182
+ duration: (duration * progress) / max,
183
+ delay: progressDelay,
184
+ useNativeDriver: false,
185
+ easing: Easing.linear,
186
+ });
187
+ Animated.parallel([progressAnimations, percentageAnim]).start();
188
+ } else {
189
+ progressAnimations.start();
190
+ }
191
+ }, [segmentTargets, animatedValue, indicator?.show, progressAnimatedValues]);
133
192
 
134
- const calculatedProgress = `${Math.round(val)}%`;
193
+ const run = useCallback(
194
+ ({ progress }: { progress: number }): void => {
195
+ // Stop ongoing animations and clear all listeners
196
+ animatedValue.stopAnimation();
197
+ animatedValue.removeAllListeners();
198
+ animatedValue.setValue(0);
199
+ progressAnimatedValues.forEach((v) => {
200
+ v.stopAnimation();
201
+ v.removeAllListeners();
202
+ v.setValue(0);
203
+ });
204
+
205
+ const targets = getProgressValues(progress);
206
+ activeProgressRef.current = progress;
135
207
 
136
- if (indicatorCircleRef?.current && tSpanRef?.current) {
137
- //@ts-ignore
208
+ // Set up indicator static properties once (not per-frame)
209
+ if (indicator?.show && indicatorCircleRef.current && tSpanRef.current) {
210
+ const calculatedProgress = `${Math.round(progress)}%`;
211
+ // @ts-ignore – setNativeProps exists on native ref
138
212
  indicatorCircleRef.current.setNativeProps({
139
- r: indicator?.radius || 0,
140
- strokeWidth: indicator?.strokeWidth || 0,
141
- cx,
142
- cy,
213
+ r: indicator.radius || 0,
214
+ strokeWidth: indicator.strokeWidth || 0,
143
215
  });
144
-
145
- //@ts-ignore
146
- tSpanRef?.current.setNativeProps({
216
+ // @ts-ignore – setNativeProps exists on native ref
217
+ tSpanRef.current.setNativeProps({
147
218
  children: calculatedProgress,
148
- dx: cx,
149
- dy: cy + 5,
150
- font: {
151
- textAnchor: 'middle',
152
- fontSize: 18,
153
- },
219
+ font: INDICATOR_FONT,
154
220
  });
155
221
  }
156
- },
157
- [radius, halfCircle, rotation, indicator?.radius, indicator?.strokeWidth]
158
- );
159
222
 
160
- const run = useCallback(
161
- ({ progress }: { progress: number }): void => {
162
- const circleProgressValues = getProgressValues(progress);
163
- progressAnimatedValues.forEach((progressAnimated, index) => {
164
- progressAnimated.addListener((v) => {
165
- if (circleRef?.current[index]) {
166
- var strokeDashoffset = circleCircumference;
167
-
168
- var val =
169
- v.value <= (circleProgressValues[index] ?? 0)
170
- ? v.value
171
- : circleProgressValues[index] ?? 0;
172
- strokeDashoffset = circleProgressValues[index]
173
- ? circleCircumference - (circleCircumference * val) / 100
174
- : circleCircumference;
175
-
176
- const paintedLength =
177
- circleCircumference -
178
- strokeDashoffset -
179
- (segments * (circleProgressValues[index] ?? 0) * segmentsGap) /
180
- 100;
181
-
182
- //@ts-ignore
183
- circleRef?.current[index]?.setNativeProps({
184
- strokeDashoffset:
185
- circleCircumference - paintedLength > circleCircumference
186
- ? circleCircumference
187
- : circleCircumference - paintedLength,
188
- });
189
- }
190
- });
191
- });
223
+ // Set up indicator position listener (trig-based, can't use interpolate)
192
224
  if (indicator?.show) {
225
+ const meanGap = getMeanSegmentsGap(progress);
193
226
  animatedValue.addListener((v) => {
194
- var strokeDashoffset = circleCircumference;
195
- var val = v.value <= progress ? v.value : progress;
196
- strokeDashoffset = progress
197
- ? circleCircumference - (circleCircumference * val) / 100
198
- : circleCircumference;
199
-
200
- const paintedLength = circleCircumference - strokeDashoffset;
227
+ const val = Math.min(v.value, progress);
228
+ const paintedLength = (circleCircumference * val) / 100;
229
+ const adjustedLength = paintedLength - meanGap;
230
+
231
+ if (adjustedLength <= 0) return;
232
+
233
+ const { x: cx, y: cy } = getArcEndCoordinates(
234
+ radius,
235
+ adjustedLength,
236
+ halfCircle,
237
+ halfCircle,
238
+ rotation
239
+ );
201
240
 
202
- const meanSegmentsGap = getMeanSegmentsGap(progress);
203
- const calculatedStrokeDashoffset = paintedLength - meanSegmentsGap;
204
- runIndicator(calculatedStrokeDashoffset, progress);
241
+ if (indicatorCircleRef.current) {
242
+ // @ts-ignore setNativeProps exists on native ref
243
+ indicatorCircleRef.current.setNativeProps({ cx, cy });
244
+ }
245
+ if (tSpanRef.current) {
246
+ // @ts-ignore – setNativeProps exists on native ref
247
+ tSpanRef.current.setNativeProps({ dx: cx, dy: cy + 5 });
248
+ }
205
249
  });
206
250
  }
207
251
 
208
- // Animate circles sequentially
209
- const progressAnimations = Animated.sequence(
210
- progressAnimatedValues.map((tav, index) =>
211
- animation(
212
- tav, // Animated value
213
- circleProgressValues[index] ?? 0, // To value
214
- index === 0 ? progressDelay : 0, // Delay
215
- (duration * (circleProgressValues[index] ?? 0)) / max // Duration
216
- )
217
- )
218
- );
219
-
220
- if (indicator?.show) {
221
- // Animate percentage circle
222
- const percentageAnim = animation(
223
- animatedValue, // Animated value
224
- progress, // To value
225
- progressDelay, // Delay
226
- (duration * progress) / max // Duration
227
- );
228
- // Progress Animations run parallelly with percentage circle
229
- Animated.parallel([progressAnimations, percentageAnim]).start();
230
- } else {
231
- progressAnimations.start();
232
- }
252
+ // Store pending config and trigger re-render with new targets.
253
+ // Animations start in a useEffect AFTER React has re-rendered,
254
+ // ensuring interpolations in progressTrack are set up correctly.
255
+ pendingAnimationRef.current = { progress, targets };
256
+ setSegmentTargets(targets);
233
257
  },
234
258
  [
235
259
  animatedValue,
236
- animation,
237
- segments,
238
260
  circleCircumference,
239
- segmentsGap,
240
261
  getMeanSegmentsGap,
241
- indicator?.show,
242
262
  getProgressValues,
243
- runIndicator,
263
+ halfCircle,
264
+ indicator?.show,
265
+ indicator?.radius,
266
+ indicator?.strokeWidth,
244
267
  progressAnimatedValues,
268
+ radius,
269
+ rotation,
245
270
  ]
246
271
  );
247
272
 
248
- const getProgress = useMemo(() => {
249
- const progressConfig = {
250
- stroke: progressColor,
251
- cx: halfCircle,
252
- cy: halfCircle,
253
- r: radius,
254
- origin: `${halfCircle}, ${halfCircle}`,
255
- strokeWidth: strokeWidth,
256
- strokeDasharray: circleCircumference,
257
- strokeDashoffset: circleCircumference,
258
- };
259
-
260
- return progressAnimatedValues.map((_, key) => (
261
- <ProgressCircle
262
- key={key}
263
- //@ts-ignore
264
- ref={(el) => (circleRef.current[key] = el)}
265
- {...progressConfig}
266
- rotation={rotation + (key * 360) / segments}
267
- strokeLinecap="round"
268
- />
269
- ));
273
+ // Memoize base track circles (static, never animate)
274
+ const baseTrack = useMemo(() => {
275
+ const baseStrokeDashoffset =
276
+ circleCircumference - circleCircumference / segments + segmentsGap;
277
+
278
+ return new Array(segments)
279
+ .fill(null)
280
+ .map((_, key) => (
281
+ <Circle
282
+ key={key}
283
+ cx={halfCircle}
284
+ cy={halfCircle}
285
+ r={radius}
286
+ stroke={baseColor}
287
+ rotation={rotation + (key * 360) / segments}
288
+ origin={`${halfCircle}, ${halfCircle}`}
289
+ strokeWidth={strokeWidth}
290
+ strokeDasharray={circleCircumference}
291
+ strokeDashoffset={baseStrokeDashoffset}
292
+ strokeLinecap="round"
293
+ />
294
+ ));
270
295
  }, [
296
+ baseColor,
297
+ circleCircumference,
298
+ halfCircle,
299
+ radius,
300
+ rotation,
271
301
  segments,
302
+ segmentsGap,
303
+ strokeWidth,
304
+ ]);
305
+
306
+ // Memoize progress overlay circles with declarative interpolated strokeDashoffset
307
+ const progressTrack = useMemo(() => {
308
+ return progressAnimatedValues.map((animVal, key) => {
309
+ const target = segmentTargets[key] ?? 0;
310
+ const strokeDashoffset = computeStrokeDashoffset(
311
+ animVal,
312
+ target,
313
+ circleCircumference,
314
+ segments,
315
+ segmentsGap
316
+ );
317
+
318
+ return (
319
+ <ProgressCircle
320
+ key={key}
321
+ stroke={progressColor}
322
+ cx={halfCircle}
323
+ cy={halfCircle}
324
+ r={radius}
325
+ origin={`${halfCircle}, ${halfCircle}`}
326
+ strokeWidth={strokeWidth}
327
+ strokeDasharray={circleCircumference}
328
+ strokeDashoffset={strokeDashoffset}
329
+ rotation={rotation + (key * 360) / segments}
330
+ strokeLinecap="round"
331
+ />
332
+ );
333
+ });
334
+ }, [
272
335
  circleCircumference,
273
336
  halfCircle,
337
+ progressAnimatedValues,
274
338
  progressColor,
275
339
  radius,
276
340
  rotation,
341
+ segmentTargets,
342
+ segments,
343
+ segmentsGap,
277
344
  strokeWidth,
278
- progressAnimatedValues,
279
345
  ]);
280
346
 
281
- useImperativeHandle(ref, () => ({ run }));
347
+ useImperativeHandle(ref, () => ({ run }), [run]);
282
348
 
283
349
  return (
284
350
  <Svg
@@ -291,28 +357,8 @@ const RNSegmentedProgressBar: ForwardRefRenderFunction<
291
357
  <View style={styles.centerComponent}>{centerComponent}</View>
292
358
  )}
293
359
  <G>
294
- {[...Array(segments)].map((_, key) => {
295
- return (
296
- <Circle
297
- key={key}
298
- cx={halfCircle}
299
- cy={halfCircle}
300
- r={radius}
301
- stroke={baseColor}
302
- rotation={rotation + (key * 360) / segments}
303
- origin={`${halfCircle}, ${halfCircle}`}
304
- strokeWidth={strokeWidth}
305
- strokeDasharray={circleCircumference}
306
- strokeDashoffset={
307
- circleCircumference -
308
- circleCircumference / segments +
309
- segmentsGap
310
- }
311
- strokeLinecap="round"
312
- />
313
- );
314
- })}
315
- {getProgress}
360
+ {baseTrack}
361
+ {progressTrack}
316
362
 
317
363
  {indicator?.show === true && (
318
364
  <>
package/.editorconfig DELETED
@@ -1,15 +0,0 @@
1
- # EditorConfig helps developers define and maintain consistent
2
- # coding styles between different editors and IDEs
3
- # editorconfig.org
4
-
5
- root = true
6
-
7
- [*]
8
-
9
- indent_style = space
10
- indent_size = 2
11
-
12
- end_of_line = lf
13
- charset = utf-8
14
- trim_trailing_whitespace = true
15
- insert_final_newline = true
package/.gitattributes DELETED
@@ -1,3 +0,0 @@
1
- *.pbxproj -text
2
- # specific for windows script files
3
- *.bat text eol=crlf
package/.gitignore DELETED
@@ -1,70 +0,0 @@
1
- # OSX
2
- #
3
- .DS_Store
4
-
5
- # XDE
6
- .expo/
7
-
8
- # VSCode
9
- .vscode/
10
- jsconfig.json
11
-
12
- # Xcode
13
- #
14
- build/
15
- *.pbxuser
16
- !default.pbxuser
17
- *.mode1v3
18
- !default.mode1v3
19
- *.mode2v3
20
- !default.mode2v3
21
- *.perspectivev3
22
- !default.perspectivev3
23
- xcuserdata
24
- *.xccheckout
25
- *.moved-aside
26
- DerivedData
27
- *.hmap
28
- *.ipa
29
- *.xcuserstate
30
- project.xcworkspace
31
-
32
- # Android/IJ
33
- #
34
- .classpath
35
- .cxx
36
- .gradle
37
- .idea
38
- .project
39
- .settings
40
- local.properties
41
- android.iml
42
-
43
- # Cocoapods
44
- #
45
- example/ios/Pods
46
-
47
- # Ruby
48
- example/vendor/
49
-
50
- # node.js
51
- #
52
- node_modules/
53
- npm-debug.log
54
- yarn-debug.log
55
- yarn-error.log
56
-
57
- # BUCK
58
- buck-out/
59
- \.buckd/
60
- android/app/libs
61
- android/keystores/debug.keystore
62
-
63
- # Expo
64
- .expo/
65
-
66
- # Turborepo
67
- .turbo/
68
-
69
- # generated by bob
70
- lib/
package/.nvmrc DELETED
@@ -1 +0,0 @@
1
- 16.18.1
package/.watchmanconfig DELETED
@@ -1 +0,0 @@
1
- {}
package/.yarnrc DELETED
@@ -1,3 +0,0 @@
1
- # Override Yarn command so we can automatically setup the repo on running `yarn`
2
-
3
- yarn-path "scripts/bootstrap.js"