@momo-kits/carousel 0.160.1-beta.12 → 0.160.1-beta.14

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 (3) hide show
  1. package/index.tsx +121 -71
  2. package/package.json +1 -1
  3. package/types.ts +2 -0
package/index.tsx CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  NativeSyntheticEvent,
17
17
  Platform,
18
18
  StyleSheet,
19
+ Text,
19
20
  View,
20
21
  } from 'react-native';
21
22
  import Animated, {
@@ -31,6 +32,11 @@ import { CarouselProps, CarouselRef } from './types';
31
32
 
32
33
  const { width: viewportWidth } = Dimensions.get('window');
33
34
 
35
+ // TEMP: force the diagnostic overlay on for every carousel in this beta so it
36
+ // shows regardless of whether the consumer screen passes `debug`. Flip to false
37
+ // (or delete) once the miniapp index bug is root-caused.
38
+ const FORCE_DEBUG = true;
39
+
34
40
  const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
35
41
 
36
42
  type CarouselItemProps = {
@@ -122,19 +128,37 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
122
128
  onTouchEnd: onTouchEndProp,
123
129
  onLayout: onLayoutProp,
124
130
  getItemLayout: getItemLayoutProp,
131
+ debug = false,
125
132
  } = props;
126
133
 
134
+ const debugEnabled = debug || FORCE_DEBUG;
135
+
127
136
  const flatListRef = useRef<FlatList>(null);
128
137
  const autoplayTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
129
138
  const scrollX = useSharedValue(0);
130
139
  const containerWidthRef = useRef(viewportWidth);
131
140
  const isAutoplayPausedRef = useRef(false);
132
141
  const currentIndexRef = useRef(firstItem);
142
+ // DEBUG: fire counters for each index-reporting source (visible via overlay).
143
+ const debugCountsRef = useRef({ s: 0, m: 0, d: 0, p: 0 });
133
144
 
134
145
  const [currentIndex, setCurrentIndex] = useState(firstItem);
135
146
  const [containerWidth, setContainerWidth] = useState(viewportWidth);
136
147
  const [isVisible, setIsVisible] = useState(apparitionDelay === 0);
137
148
  const [isLayoutReady, setIsLayoutReady] = useState(false);
149
+ const [debugInfo, setDebugInfo] = useState({
150
+ s: 0,
151
+ m: 0,
152
+ d: 0,
153
+ p: 0,
154
+ tag: '-',
155
+ off: 0,
156
+ lw: 0,
157
+ cw: 0,
158
+ iw: 0,
159
+ idx: -1,
160
+ real: -1,
161
+ });
138
162
 
139
163
  const itemWidth = useMemo(() => {
140
164
  if (full) {
@@ -355,99 +379,92 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
355
379
  [dataWithClones.length, itemWidth]
356
380
  );
357
381
 
358
- const handleScroll = useCallback(
359
- (offsetX: number, layoutWidth: number, contentWidth: number) => {
382
+ // Single index reporter shared by every source (scroll frames + settle
383
+ // events). `tag` identifies which source fired, surfaced in the debug overlay.
384
+ const reportResting = useCallback(
385
+ (offsetX: number, layoutWidth: number, contentWidth: number, tag: string) => {
360
386
  const index = resolveIndex(offsetX, layoutWidth, contentWidth);
361
387
  const realIndex = getRealIndex(index);
388
+ const changed = realIndex !== currentIndexRef.current;
389
+
390
+ if (debugEnabled && (tag !== 's' || changed)) {
391
+ const c = debugCountsRef.current;
392
+ setDebugInfo({
393
+ s: c.s,
394
+ m: c.m,
395
+ d: c.d,
396
+ p: c.p,
397
+ tag,
398
+ off: offsetX,
399
+ lw: layoutWidth,
400
+ cw: contentWidth,
401
+ iw: itemWidth,
402
+ idx: index,
403
+ real: realIndex,
404
+ });
405
+ }
362
406
 
363
- if (realIndex !== currentIndexRef.current) {
364
- // eslint-disable-next-line no-console
365
- console.log(
366
- `[Carousel] SCROLL idx=${index} real=${realIndex} offsetX=${offsetX.toFixed(1)} layoutW=${layoutWidth.toFixed(1)} contentW=${contentWidth.toFixed(1)} itemWidth=${itemWidth.toFixed(1)} count=${dataWithClones.length}`
367
- );
407
+ if (changed) {
368
408
  currentIndexRef.current = realIndex;
369
409
  setCurrentIndex(realIndex);
370
-
371
410
  if (onScrollIndexChanged) {
372
411
  onScrollIndexChanged(realIndex);
373
412
  }
374
413
  }
414
+ return index;
375
415
  },
376
- [resolveIndex, getRealIndex, onScrollIndexChanged, itemWidth, dataWithClones.length]
416
+ [resolveIndex, getRealIndex, onScrollIndexChanged, debugEnabled, itemWidth]
377
417
  );
378
418
 
379
- const handleMomentumScrollEnd = useCallback(
380
- (event: NativeSyntheticEvent<NativeScrollEvent>) => {
381
- const { contentOffset, layoutMeasurement, contentSize } =
382
- event.nativeEvent;
383
- const offsetX = contentOffset.x;
384
- const index = resolveIndex(
385
- offsetX,
386
- layoutMeasurement.width,
387
- contentSize.width
388
- );
389
- const realIndex = getRealIndex(index);
390
-
391
- // eslint-disable-next-line no-console
392
- console.log(
393
- `[Carousel] MOMENTUM_END idx=${index} real=${realIndex} offsetX=${offsetX.toFixed(1)} layoutW=${layoutMeasurement.width.toFixed(1)} contentW=${contentSize.width.toFixed(1)} itemWidth=${itemWidth.toFixed(1)} count=${dataWithClones.length}`
394
- );
395
-
396
- // Report the final resting index here as well. The reanimated scroll
397
- // handler isn't guaranteed to deliver the last momentum frame to JS in
398
- // every runtime (e.g. miniapp hosts), so relying on handleScroll alone
399
- // can leave onScrollIndexChanged stuck one item short of the end.
400
- if (realIndex !== currentIndexRef.current) {
401
- currentIndexRef.current = realIndex;
402
- setCurrentIndex(realIndex);
403
-
404
- if (onScrollIndexChanged) {
405
- onScrollIndexChanged(realIndex);
406
- }
407
- }
419
+ const handleScroll = useCallback(
420
+ (offsetX: number, layoutWidth: number, contentWidth: number) => {
421
+ debugCountsRef.current.s += 1;
422
+ reportResting(offsetX, layoutWidth, contentWidth, 's');
423
+ },
424
+ [reportResting]
425
+ );
408
426
 
427
+ // Reliable settle path: reanimated owns the native scroll subscription, so its
428
+ // onMomentumEnd / onEndDrag worklets fire even when the plain FlatList
429
+ // onMomentumScrollEnd prop doesn't (observed in the miniapp host).
430
+ const handleMomentumSettle = useCallback(
431
+ (offsetX: number, layoutWidth: number, contentWidth: number) => {
432
+ debugCountsRef.current.m += 1;
433
+ const index = reportResting(offsetX, layoutWidth, contentWidth, 'm');
409
434
  if (onSnapToItem) {
410
- onSnapToItem(realIndex);
435
+ onSnapToItem(getRealIndex(index));
411
436
  }
412
-
413
437
  handleLoopReposition(index);
438
+ },
439
+ [reportResting, onSnapToItem, getRealIndex, handleLoopReposition]
440
+ );
414
441
 
415
- if (onMomentumScrollEndProp) {
416
- onMomentumScrollEndProp(event);
417
- }
442
+ const handleDragSettle = useCallback(
443
+ (offsetX: number, layoutWidth: number, contentWidth: number) => {
444
+ debugCountsRef.current.d += 1;
445
+ reportResting(offsetX, layoutWidth, contentWidth, 'd');
418
446
  },
419
- [resolveIndex, getRealIndex, onScrollIndexChanged, onSnapToItem, handleLoopReposition, onMomentumScrollEndProp, itemWidth, dataWithClones.length]
447
+ [reportResting]
420
448
  );
421
449
 
422
- const handleScrollEndDrag = useCallback(
450
+ // Kept so the consumer's onMomentumScrollEnd prop still fires, and to measure
451
+ // (counter `p`) whether the plain FlatList momentum event reaches JS at all.
452
+ const handleMomentumScrollEnd = useCallback(
423
453
  (event: NativeSyntheticEvent<NativeScrollEvent>) => {
424
- // Fallback for a drag released without enough velocity to fire a momentum
425
- // scroll: report the resting index so onScrollIndexChanged isn't left
426
- // stuck one item short of the end.
427
454
  const { contentOffset, layoutMeasurement, contentSize } =
428
455
  event.nativeEvent;
429
- const index = resolveIndex(
456
+ debugCountsRef.current.p += 1;
457
+ reportResting(
430
458
  contentOffset.x,
431
459
  layoutMeasurement.width,
432
- contentSize.width
433
- );
434
- const realIndex = getRealIndex(index);
435
-
436
- // eslint-disable-next-line no-console
437
- console.log(
438
- `[Carousel] DRAG_END idx=${index} real=${realIndex} offsetX=${contentOffset.x.toFixed(1)} layoutW=${layoutMeasurement.width.toFixed(1)} contentW=${contentSize.width.toFixed(1)} itemWidth=${itemWidth.toFixed(1)} count=${dataWithClones.length}`
460
+ contentSize.width,
461
+ 'p'
439
462
  );
440
-
441
- if (realIndex !== currentIndexRef.current) {
442
- currentIndexRef.current = realIndex;
443
- setCurrentIndex(realIndex);
444
-
445
- if (onScrollIndexChanged) {
446
- onScrollIndexChanged(realIndex);
447
- }
463
+ if (onMomentumScrollEndProp) {
464
+ onMomentumScrollEndProp(event);
448
465
  }
449
466
  },
450
- [resolveIndex, getRealIndex, onScrollIndexChanged, itemWidth, dataWithClones.length]
467
+ [reportResting, onMomentumScrollEndProp]
451
468
  );
452
469
 
453
470
  const handleTouchStart = useCallback(
@@ -477,10 +494,6 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
477
494
  const handleLayout = useCallback(
478
495
  (event: LayoutChangeEvent) => {
479
496
  const { width } = event.nativeEvent.layout;
480
- // eslint-disable-next-line no-console
481
- console.log(
482
- `[Carousel] LAYOUT width=${width.toFixed(1)} window=${viewportWidth.toFixed(1)} full=${full}`
483
- );
484
497
  containerWidthRef.current = width;
485
498
  setContainerWidth(width);
486
499
  setIsLayoutReady(true);
@@ -489,7 +502,7 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
489
502
  onLayoutProp(event);
490
503
  }
491
504
  },
492
- [onLayoutProp, full]
505
+ [onLayoutProp]
493
506
  );
494
507
 
495
508
  const getItemLayout = useCallback(
@@ -575,6 +588,20 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
575
588
  );
576
589
  runOnJS(emitScrollProp)(event.contentOffset.x);
577
590
  },
591
+ onMomentumEnd: (event) => {
592
+ runOnJS(handleMomentumSettle)(
593
+ event.contentOffset.x,
594
+ event.layoutMeasurement.width,
595
+ event.contentSize.width,
596
+ );
597
+ },
598
+ onEndDrag: (event) => {
599
+ runOnJS(handleDragSettle)(
600
+ event.contentOffset.x,
601
+ event.layoutMeasurement.width,
602
+ event.contentSize.width,
603
+ );
604
+ },
578
605
  });
579
606
 
580
607
  useEffect(() => {
@@ -635,7 +662,6 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
635
662
  scrollEventThrottle={16}
636
663
  onScroll={onScrollEvent}
637
664
  onMomentumScrollEnd={handleMomentumScrollEnd}
638
- onScrollEndDrag={handleScrollEndDrag}
639
665
  onTouchStart={handleTouchStart}
640
666
  onTouchEnd={handleTouchEnd}
641
667
  snapToOffsets={snapOffsets}
@@ -654,6 +680,16 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
654
680
  maxToRenderPerBatch={5}
655
681
  windowSize={5}
656
682
  />
683
+ {debugEnabled && (
684
+ <View style={styles.debugOverlay} pointerEvents="none">
685
+ <Text style={styles.debugText}>
686
+ {`S${debugInfo.s} M${debugInfo.m} D${debugInfo.d} P${debugInfo.p} [${debugInfo.tag}] idx=${debugInfo.idx} real=${debugInfo.real}`}
687
+ </Text>
688
+ <Text style={styles.debugText}>
689
+ {`off=${debugInfo.off.toFixed(0)} lW=${debugInfo.lw.toFixed(0)} cW=${debugInfo.cw.toFixed(0)} iw=${debugInfo.iw.toFixed(0)}`}
690
+ </Text>
691
+ </View>
692
+ )}
657
693
  </View>
658
694
  );
659
695
  });
@@ -662,6 +698,20 @@ const styles = StyleSheet.create({
662
698
  container: {
663
699
  overflow: 'hidden',
664
700
  },
701
+ debugOverlay: {
702
+ position: 'absolute',
703
+ top: 4,
704
+ left: 4,
705
+ backgroundColor: 'rgba(0,0,0,0.7)',
706
+ paddingHorizontal: 6,
707
+ paddingVertical: 4,
708
+ borderRadius: 4,
709
+ },
710
+ debugText: {
711
+ color: '#0f0',
712
+ fontSize: 11,
713
+ fontWeight: '700',
714
+ },
665
715
  });
666
716
 
667
717
  Carousel.displayName = 'Carousel';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/carousel",
3
- "version": "0.160.1-beta.12",
3
+ "version": "0.160.1-beta.14",
4
4
  "private": false,
5
5
  "main": "index.tsx",
6
6
  "peerDependencies": {
package/types.ts CHANGED
@@ -43,6 +43,8 @@ export type CarouselProps = {
43
43
  contentContainerStyle?: StyleProp<ViewStyle>;
44
44
  full?: boolean;
45
45
  snapToInterval?: number | Animated.Value | Animated.AnimatedInterpolation<number | string> | undefined
46
+ /** Renders an on-screen diagnostic overlay (fire counters + native scroll geometry). */
47
+ debug?: boolean;
46
48
  };
47
49
 
48
50
  export type CarouselRef = {