@onekeyfe/react-native-pager-view 3.0.180 → 6.7.0-2

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 (31) hide show
  1. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +2 -2
  2. package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +2 -2
  3. package/docs/SPEC.md +31 -4
  4. package/ios/RNCCollapsiblePagerViewComponentView.mm +84 -21
  5. package/ios/RNCNativeScrollerComponentView.mm +6 -5
  6. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +26 -1
  7. package/ios/tests/RNCNativeScrollerTests.mm +39 -0
  8. package/lib/module/CollapsiblePagerView.js +4 -4
  9. package/lib/module/CollapsiblePagerViewNativeComponent.ts +1 -1
  10. package/lib/module/NativeScroller.js +2 -2
  11. package/lib/module/NativeScrollerNativeComponent.ts +1 -1
  12. package/lib/module/ScrollEdgeEffect.js +8 -0
  13. package/lib/module/index.js +1 -0
  14. package/lib/module/index.web.js +1 -0
  15. package/lib/typescript/src/CollapsiblePagerView.d.ts +4 -3
  16. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +2 -1
  17. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +1 -1
  18. package/lib/typescript/src/NativeScroller.d.ts +3 -2
  19. package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +1 -1
  20. package/lib/typescript/src/ScrollEdgeEffect.d.ts +5 -0
  21. package/lib/typescript/src/index.d.ts +1 -0
  22. package/lib/typescript/src/index.web.d.ts +1 -0
  23. package/package.json +2 -2
  24. package/src/CollapsiblePagerView.tsx +12 -7
  25. package/src/CollapsiblePagerView.web.tsx +2 -1
  26. package/src/CollapsiblePagerViewNativeComponent.ts +1 -1
  27. package/src/NativeScroller.tsx +6 -4
  28. package/src/NativeScrollerNativeComponent.ts +1 -1
  29. package/src/ScrollEdgeEffect.ts +4 -0
  30. package/src/index.tsx +2 -0
  31. package/src/index.web.tsx +2 -0
@@ -207,8 +207,8 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
207
207
  }
208
208
 
209
209
  // The scroll-edge effect and external overlay are UIKit-only.
210
- @ReactProp(name = "topScrollEdgeHard", defaultBoolean = false)
211
- override fun setTopScrollEdgeHard(view: CollapsiblePagerHost?, value: Boolean) = Unit
210
+ @ReactProp(name = "topScrollEdgeEffect")
211
+ override fun setTopScrollEdgeEffect(view: CollapsiblePagerHost?, value: String?) = Unit
212
212
  @ReactProp(name = "topOverlayColorScheme")
213
213
  override fun setTopOverlayColorScheme(view: CollapsiblePagerHost?, value: String?) = Unit
214
214
  @ReactProp(name = "topOverlayHeight", defaultDouble = 0.0)
@@ -49,8 +49,8 @@ class NativeScrollerViewManager : ViewGroupManager<NativeScrollerView>(),
49
49
  override fun setContentWidth(view: NativeScrollerView, value: Double) = view.setContentSize(width = view.dip(value))
50
50
  @ReactProp(name = "contentHeight")
51
51
  override fun setContentHeight(view: NativeScrollerView, value: Double) = view.setContentSize(height = view.dip(value))
52
- @ReactProp(name = "topScrollEdgeHard", defaultBoolean = false)
53
- override fun setTopScrollEdgeHard(view: NativeScrollerView, value: Boolean) = Unit
52
+ @ReactProp(name = "topScrollEdgeEffect")
53
+ override fun setTopScrollEdgeEffect(view: NativeScrollerView, value: String?) = Unit
54
54
  @ReactProp(name = "topOverlayHeight", defaultDouble = 0.0)
55
55
  override fun setTopOverlayHeight(view: NativeScrollerView, value: Double) = Unit
56
56
  @ReactProp(name = "scrollEnabled", defaultBoolean = true)
package/docs/SPEC.md CHANGED
@@ -7,13 +7,17 @@ scroll-edge props.
7
7
  ## iOS 26 top scroll edge overlay
8
8
 
9
9
  `CollapsiblePagerView` and standalone `NativeScroller` accept the optional
10
- `topScrollEdgeEffect="hard"` with a fixed `topOverlay` React child. Both are
10
+ `topScrollEdgeEffect={ScrollEdgeEffect.Hard}` or `ScrollEdgeEffect.Soft` with a
11
+ fixed `topOverlay` React child. `ScrollEdgeEffect` is an exported string enum
12
+ (`Hard = "hard"`, `Soft = "soft"`); the corresponding string literals remain
13
+ valid prop values. Both the style and overlay are
11
14
  disabled by default. On iOS 26 or newer, a local overlay remains fixed at the
12
15
  top of the viewport while the owned vertical `UIScrollView` can draw behind it.
13
16
  The component measures the overlay's Fabric layout height and owns the
14
17
  corresponding top content inset. It binds UIKit's
15
18
  `UIScrollEdgeElementContainerInteraction` to that exact scroll view and selects
16
- the hard top edge style. A pager owns the effect and inset for its current page;
19
+ the requested top edge style. Hard has UIKit's dividing line; Soft has its
20
+ gradual blurred transition. A pager owns the effect and inset for its current page;
17
21
  its nested NativeList or NativeScroller must not opt into another top overlay.
18
22
  Invisible, noninteractive native labels let UIKit recognize the React-drawn
19
23
  overlay and sticky header as scroll-edge elements. The pager does not add an
@@ -28,7 +32,7 @@ The pager weakly observes its geometry in the same window, retaining its own
28
32
  noninteractive native proxy for UIKit recognition without reparenting or
29
33
  modifying the React header. The measured external header bottom supplies the
30
34
  top inset and sticky anchor. A local `topOverlay` takes precedence. The tag
31
- alone never enables the effect: `topScrollEdgeEffect="hard"` controls the
35
+ alone never enables the effect: either `topScrollEdgeEffect` enum value controls the
32
36
  interaction, while valid external geometry can reserve the top inset before
33
37
  the effect is enabled. Before the tag and height are ready, the caller keeps
34
38
  its existing layout; an unresolved native tag has zero external height. An
@@ -47,7 +51,7 @@ When a smooth shared header reaches the overlay boundary, the existing sticky
47
51
  React and native tab views move together into a view pinned to the active
48
52
  scroll view. Their identities, scroll-view gesture ancestry and hit targets
49
53
  remain intact; the collapsible header stays in the scrolling header host.
50
- Without a local or external overlay, `topScrollEdgeEffect="hard"` may protect
54
+ Without a local or external overlay, either scroll-edge style may protect
51
55
  only the genuinely pinned sticky header; it must not consume a page as a
52
56
  synthetic overlay slot or change the collapsible banner distance.
53
57
  Changing pages, replacing the active list or recycling returns those views to
@@ -69,6 +73,29 @@ Acceptance requires real iOS 26 scrolling, partial collapse, short/empty page,
69
73
  refresh, page transition/cancellation, dynamic tabs and light/dark checks;
70
74
  source and build checks alone do not establish visual correctness.
71
75
 
76
+ Experimental trial: `CollapsiblePagerView` paints a cover at each owned
77
+ fixed-overlay or pinned-sticky boundary with one physical pixel of the nearest
78
+ opaque ancestor background color (system background if none is available).
79
+ These native layers do not receive touches, change insets, or clip glass shadows.
80
+ They are removed for Soft, disabled/detached effects, and recycling. This is a
81
+ visual workaround using public layer APIs, not a UIKit separator setting; rendered
82
+ light/dark, scrolling, and transition acceptance is pending. Standalone
83
+ NativeScroller and NativeList retain UIKit's unmodified Hard appearance.
84
+ On the iOS 26.5 simulator, a runtime probe confirmed Hard and the cover layers,
85
+ including pinned-header ownership. The original hairline was not reliably
86
+ reproduced with the covers hidden, so separator suppression is not yet verified.
87
+
88
+ Style selection crosses Fabric as the enum `none | hard | soft`, defaulting to
89
+ `none`, rather than a hard-effect boolean. Switching Hard ↔ Soft on an attached
90
+ owner updates UIKit without replacing the overlay or changing its inset,
91
+ logical scroll offset, or saved original style. Clearing the prop, detaching,
92
+ or recycling releases the interaction and restores that original style.
93
+ This bridge contract requires a matching native rebuild. Soft selection is
94
+ implemented in the public wrappers and native hosts. Focused JS tests cover
95
+ style forwarding, overlay retention and disabling; UIKit tests cover native
96
+ style switching and restoring the previous style. Rendered iOS acceptance must
97
+ be reported separately from type, codegen and build checks.
98
+
72
99
  ## Smooth shared-header and per-page body memory
73
100
 
74
101
  With nativeSmoothHeaderScrollEnabled, the header collapse C is shared across all
@@ -1467,6 +1467,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1467
1467
  - (void)cancelTopInsetReboundCheck;
1468
1468
  - (void)scheduleTopInsetReboundCheck;
1469
1469
  - (void)syncScrollEdgeAppearance;
1470
+ - (void)updateHardEdgeSeparatorCovers;
1470
1471
  - (void)updateScrollEdgeContentMasks;
1471
1472
  - (void)restoreScrollEdgeContentMasks;
1472
1473
  - (void)sharedHeaderPressCancellationGestureChanged:(UIPanGestureRecognizer *)recognizer;
@@ -1503,6 +1504,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1503
1504
  UIScrollEdgeElementContainerInteraction *_stickyEdgeInteraction;
1504
1505
  UILabel *_topEdgeMarker;
1505
1506
  UILabel *_stickyEdgeMarker;
1507
+ CALayer *_topEdgeSeparatorCover;
1508
+ CALayer *_stickyEdgeSeparatorCover;
1506
1509
  __weak UIView *_topEdgeInteractionHost;
1507
1510
  __weak UIView *_stickyEdgeInteractionHost;
1508
1511
  __weak UIScrollView *_topEdgeScrollView;
@@ -1521,7 +1524,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1521
1524
  __weak RNCPagerViewComponentView *_externalOuterPager;
1522
1525
  BOOL _externalOuterPageActive;
1523
1526
  BOOL _externalOuterTransitioning;
1524
- BOOL _topScrollEdgeHard;
1527
+ BOOL _topScrollEdgeEnabled;
1528
+ RNCCollapsiblePagerViewTopScrollEdgeEffect _topScrollEdgeEffect;
1525
1529
  NSString *_topOverlayColorScheme;
1526
1530
  BOOL _pendingTopOverlayAppearanceCommit;
1527
1531
  BOOL _applyingTopOverlayInsetChange;
@@ -1914,7 +1918,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1914
1918
 
1915
1919
  - (void)syncScrollEdgeAppearance
1916
1920
  {
1917
- BOOL active = _topScrollEdgeHard &&
1921
+ BOOL active = _topScrollEdgeEnabled &&
1918
1922
  (self.hasFixedTopOverlay || _stickyHeaderView != nil) && self.window != nil &&
1919
1923
  !_isBeingRecycled;
1920
1924
  UIUserInterfaceStyle style = UIUserInterfaceStyleUnspecified;
@@ -1952,7 +1956,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1952
1956
 
1953
1957
  - (void)updateScrollEdgeContentMasks
1954
1958
  {
1955
- BOOL enabled = _topScrollEdgeHard &&
1959
+ [self updateHardEdgeSeparatorCovers];
1960
+ BOOL enabled = _topScrollEdgeEnabled &&
1956
1961
  (self.hasFixedTopOverlay || _stickyHeaderView != nil) &&
1957
1962
  self.window != nil && !_isBeingRecycled;
1958
1963
  if (!enabled) {
@@ -2052,9 +2057,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2052
2057
  BOOL externalOwnerReady = _externalTopOverlayTag <= 0 ||
2053
2058
  _externalOuterPager == nil ||
2054
2059
  (_externalOuterPageActive && !_externalOuterTransitioning);
2055
- BOOL hasOverlay = _topScrollEdgeHard && self.hasFixedTopOverlay &&
2060
+ BOOL hasOverlay = _topScrollEdgeEnabled && self.hasFixedTopOverlay &&
2056
2061
  externalOwnerReady && self.window != nil;
2057
- BOOL hasSticky = _topScrollEdgeHard && _stickyHeaderView != nil &&
2062
+ BOOL hasSticky = _topScrollEdgeEnabled && _stickyHeaderView != nil &&
2058
2063
  externalOwnerReady && self.window != nil;
2059
2064
  BOOL enabled = (hasOverlay || hasSticky) && scrollView != nil;
2060
2065
  BOOL topHostChanged = (_topEdgeInteraction != nil) != hasOverlay ||
@@ -2080,7 +2085,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2080
2085
  if (enabled && _topEdgeScrollView == nil) {
2081
2086
  _originalTopEdgeStyle = scrollView.topEdgeEffect.style;
2082
2087
  _topEdgeScrollView = scrollView;
2083
- scrollView.topEdgeEffect.style = UIScrollEdgeEffectStyle.hardStyle;
2088
+ }
2089
+ if (enabled) {
2090
+ UIScrollEdgeEffectStyle *style =
2091
+ _topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::Soft
2092
+ ? UIScrollEdgeEffectStyle.softStyle : UIScrollEdgeEffectStyle.hardStyle;
2093
+ if (scrollView.topEdgeEffect.style != style) scrollView.topEdgeEffect.style = style;
2084
2094
  }
2085
2095
  if (hasOverlay && enabled && _topEdgeInteraction == nil) {
2086
2096
  // UIKit needs a native element to shape the edge effect around React-drawn content.
@@ -2135,6 +2145,54 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2135
2145
  }
2136
2146
  }
2137
2147
 
2148
+ - (void)updateHardEdgeSeparatorCovers
2149
+ {
2150
+ BOOL enabled = _topScrollEdgeEnabled &&
2151
+ _topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard &&
2152
+ _topEdgeScrollView != nil && self.window != nil && !_isBeingRecycled;
2153
+ if (!enabled && _topEdgeSeparatorCover == nil && _stickyEdgeSeparatorCover == nil) return;
2154
+ UIColor *color = nil;
2155
+ for (UIView *view = self; view != nil; view = view.superview) {
2156
+ UIColor *candidate = [view.backgroundColor resolvedColorWithTraitCollection:
2157
+ _topOverlayHostView.traitCollection];
2158
+ if (candidate != nil && CGColorGetAlpha(candidate.CGColor) >= 1) {
2159
+ color = candidate;
2160
+ break;
2161
+ }
2162
+ }
2163
+ color = color ?: [UIColor.systemBackgroundColor resolvedColorWithTraitCollection:
2164
+ _topOverlayHostView.traitCollection];
2165
+ CGFloat scale = self.window.screen.scale ?: self.traitCollection.displayScale;
2166
+ CGFloat pixel = 1.0 / MAX(1, scale);
2167
+ UIView *hosts[] = {_topEdgeInteractionHost, _stickyEdgeInteractionHost};
2168
+ CALayer *covers[] = {_topEdgeSeparatorCover, _stickyEdgeSeparatorCover};
2169
+ for (NSInteger index = 0; index < 2; index++) {
2170
+ UIView *host = hosts[index];
2171
+ CALayer *cover = covers[index];
2172
+ if (!enabled || host == nil || host.hidden || CGRectIsEmpty(host.bounds)) {
2173
+ [cover removeFromSuperlayer];
2174
+ cover = nil;
2175
+ } else {
2176
+ if (cover == nil) {
2177
+ cover = [CALayer layer];
2178
+ cover.name = @"onekey-hard-edge-separator-cover";
2179
+ cover.zPosition = 1;
2180
+ cover.actions = @{@"position": NSNull.null, @"bounds": NSNull.null,
2181
+ @"backgroundColor": NSNull.null};
2182
+ [_containerView.layer addSublayer:cover];
2183
+ }
2184
+ // Cover only the hairline; keep the native Hard material and glass shadows.
2185
+ CGFloat boundary = CGRectGetMaxY([host convertRect:host.bounds toView:_containerView]);
2186
+ cover.frame = CGRectMake(0, round(boundary * scale) / scale - pixel,
2187
+ CGRectGetWidth(_containerView.bounds), pixel);
2188
+ cover.contentsScale = scale;
2189
+ cover.backgroundColor = color.CGColor;
2190
+ }
2191
+ if (index == 0) _topEdgeSeparatorCover = cover;
2192
+ else _stickyEdgeSeparatorCover = cover;
2193
+ }
2194
+ }
2195
+
2138
2196
  - (void)dealloc
2139
2197
  {
2140
2198
  [self updateTopScrollEdgeForScrollView:nil];
@@ -2321,7 +2379,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2321
2379
  if (_sharedHeaderScrollView == scrollView &&
2322
2380
  _sharedHeaderHostView.superview == scrollView) {
2323
2381
  _sharedHeaderHostView.layer.zPosition =
2324
- _topScrollEdgeHard && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
2382
+ _topScrollEdgeEnabled && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
2325
2383
  [self layoutSharedHeaders];
2326
2384
  return;
2327
2385
  }
@@ -2329,7 +2387,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2329
2387
  _sharedHeaderScrollView = scrollView;
2330
2388
  _sharedHeadersLiftedForPagerTransition = NO;
2331
2389
  _sharedHeaderHostView.layer.zPosition =
2332
- _topScrollEdgeHard && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
2390
+ _topScrollEdgeEnabled && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
2333
2391
  if (_sharedHeaderHostView.superview != scrollView) {
2334
2392
  [scrollView addSubview:_sharedHeaderHostView];
2335
2393
  }
@@ -2424,7 +2482,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2424
2482
  CGFloat pinnedTranslation = MAX(0, _currentLogicalOffset - _headerHeight - refreshInset);
2425
2483
  CGFloat stickyTop = [_sharedHeaderHostView convertPoint:
2426
2484
  CGPointMake(0, _headerHeight + pinnedTranslation) toView:self].y;
2427
- BOOL shouldPin = _topScrollEdgeHard &&
2485
+ BOOL shouldPin = _topScrollEdgeEnabled &&
2428
2486
  _stickyHeaderView != nil && attachedToList && !_transitioning &&
2429
2487
  !_externalOuterTransitioning &&
2430
2488
  !_isPagerDragging && _headerOffset >= _headerHeight - 0.5 &&
@@ -2646,7 +2704,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2646
2704
  _topOverlayView.window == self.window;
2647
2705
  BOOL externalOverlayReady = _externalTopOverlayTag > 0 &&
2648
2706
  _externalTopOverlayView.window == self.window;
2649
- if (!_isBeingRecycled && _topScrollEdgeHard && self.window != nil &&
2707
+ if (!_isBeingRecycled && _topScrollEdgeEnabled && self.window != nil &&
2650
2708
  (localOverlayReady || externalOverlayReady)) {
2651
2709
  [self updateTopScrollEdgeForScrollView:_observedScrollView];
2652
2710
  }
@@ -2729,7 +2787,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2729
2787
  if (_isBeingRecycled) return;
2730
2788
  NSInteger safeIndex = MIN(MAX(index, 0), _logicalChildren.count);
2731
2789
  [_logicalChildren insertObject:childComponentView atIndex:safeIndex];
2732
- if (_topScrollEdgeHard || _topOverlayView != nil) {
2790
+ if (_topScrollEdgeEnabled || _topOverlayView != nil) {
2733
2791
  _needsSlotRebuild = YES;
2734
2792
  } else {
2735
2793
  [self rebuildSlots];
@@ -2744,7 +2802,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2744
2802
  if (existingIndex == NSNotFound) return;
2745
2803
  [childComponentView removeFromSuperview];
2746
2804
  [_logicalChildren removeObjectAtIndex:existingIndex];
2747
- if (_topScrollEdgeHard || _topOverlayView != nil) {
2805
+ if (_topScrollEdgeEnabled || _topOverlayView != nil) {
2748
2806
  _needsSlotRebuild = YES;
2749
2807
  } else {
2750
2808
  [self rebuildSlots];
@@ -2761,7 +2819,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2761
2819
  _needsSlotRebuild = YES;
2762
2820
  return;
2763
2821
  }
2764
- if (_topScrollEdgeHard &&
2822
+ if (_topScrollEdgeEnabled &&
2765
2823
  _logicalChildren.count != _pageKeys.count + 2 &&
2766
2824
  _logicalChildren.count != _pageKeys.count + 3) {
2767
2825
  _needsSlotRebuild = YES;
@@ -2800,7 +2858,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2800
2858
  _stickyHeaderView = _logicalChildren[1];
2801
2859
  [_sharedHeaderHostView addSubview:_stickyHeaderView];
2802
2860
  }
2803
- BOOL hasOverlaySlot = _topScrollEdgeHard &&
2861
+ BOOL hasOverlaySlot = _topScrollEdgeEnabled &&
2804
2862
  _logicalChildren.count == _pageKeys.count + 3;
2805
2863
  NSInteger pageSlotStart = hasOverlaySlot ? 3 : 2;
2806
2864
  if (hasOverlaySlot) {
@@ -3050,7 +3108,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3050
3108
  _externalTopOverlayHeight = 0;
3051
3109
  _externalOuterPageActive = NO;
3052
3110
  _externalOuterTransitioning = NO;
3053
- _topScrollEdgeHard = NO;
3111
+ _topScrollEdgeEnabled = NO;
3112
+ _topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::None;
3054
3113
  _nativeTabBarHeight = 44;
3055
3114
  _nativeSubHeaderHeight = 74;
3056
3115
  _headerOffset = 0;
@@ -3092,7 +3151,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3092
3151
  const auto &oldViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(_props);
3093
3152
  const auto &newViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(props);
3094
3153
  _applyingTopOverlayInsetChange =
3095
- oldViewProps.topScrollEdgeHard != newViewProps.topScrollEdgeHard ||
3154
+ (oldViewProps.topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::None) !=
3155
+ (newViewProps.topScrollEdgeEffect == RNCCollapsiblePagerViewTopScrollEdgeEffect::None) ||
3096
3156
  oldViewProps.topOverlayHeight != newViewProps.topOverlayHeight;
3097
3157
 
3098
3158
  if (_needsPropsReapply || oldViewProps.initialPage != newViewProps.initialPage) {
@@ -3143,9 +3203,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3143
3203
  _stickyHeaderHeight = MAX(0, newViewProps.stickyHeaderHeight);
3144
3204
  insetsChanged = YES;
3145
3205
  }
3146
- if (_needsPropsReapply || oldViewProps.topScrollEdgeHard != newViewProps.topScrollEdgeHard) {
3147
- _topScrollEdgeHard = newViewProps.topScrollEdgeHard;
3148
- if (oldViewProps.topScrollEdgeHard || newViewProps.topScrollEdgeHard) {
3206
+ if (_needsPropsReapply || oldViewProps.topScrollEdgeEffect != newViewProps.topScrollEdgeEffect) {
3207
+ BOOL enabled = newViewProps.topScrollEdgeEffect != RNCCollapsiblePagerViewTopScrollEdgeEffect::None;
3208
+ BOOL enabledChanged = _topScrollEdgeEnabled != enabled;
3209
+ _topScrollEdgeEnabled = enabled;
3210
+ _topScrollEdgeEffect = newViewProps.topScrollEdgeEffect;
3211
+ if (enabledChanged) {
3149
3212
  _deferTopEdgeSlotRebuild = YES;
3150
3213
  [self rebuildSlots];
3151
3214
  NSUInteger generation = _generation;
@@ -3156,12 +3219,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3156
3219
  [self rebuildSlots];
3157
3220
  });
3158
3221
  }
3159
- insetsChanged = YES;
3222
+ insetsChanged |= enabledChanged;
3160
3223
  }
3161
3224
  if (_needsPropsReapply ||
3162
3225
  oldViewProps.topOverlayColorScheme != newViewProps.topOverlayColorScheme) {
3163
3226
  NSString *nextScheme = RCTNSStringFromString(newViewProps.topOverlayColorScheme);
3164
- if (self.hasFixedTopOverlay && _topScrollEdgeHard &&
3227
+ if (self.hasFixedTopOverlay && _topScrollEdgeEnabled &&
3165
3228
  _topOverlayColorScheme != nil &&
3166
3229
  ![_topOverlayColorScheme isEqualToString:nextScheme]) {
3167
3230
  _pendingTopOverlayAppearanceCommit = YES;
@@ -196,7 +196,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
196
196
  BOOL refreshEnded = previous.refreshing && !next.refreshing;
197
197
  [super updateProps:props oldProps:oldProps];
198
198
  [self updateTopScrollEdge];
199
- if (next.topScrollEdgeHard && _topOverlay == nil) {
199
+ if ((next.topScrollEdgeEffect != RNCNativeScrollerTopScrollEdgeEffect::None) && _topOverlay == nil) {
200
200
  __weak __typeof__(self) weakSelf = self;
201
201
  dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf updateTopScrollEdge]; });
202
202
  }
@@ -210,7 +210,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
210
210
  if (@available(iOS 26.0, *)) {
211
211
  const auto &props = *std::static_pointer_cast<const RNCNativeScrollerProps>(_props);
212
212
  UIView *overlay = nil;
213
- if (props.topScrollEdgeHard && !_recycled) {
213
+ if ((props.topScrollEdgeEffect != RNCNativeScrollerTopScrollEdgeEffect::None) && !_recycled) {
214
214
  NSUInteger fabricSiblings = 0;
215
215
  for (UIView *candidate in self.superview.subviews) {
216
216
  if (![candidate conformsToProtocol:@protocol(RCTComponentViewProtocol)]) continue;
@@ -220,7 +220,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
220
220
  if (fabricSiblings != 2) overlay = nil;
221
221
  }
222
222
  _topOverlay = overlay;
223
- CGFloat requestedOverlayInset = props.topScrollEdgeHard && !_recycled && overlay != nil &&
223
+ CGFloat requestedOverlayInset = (props.topScrollEdgeEffect != RNCNativeScrollerTopScrollEdgeEffect::None) && !_recycled && overlay != nil &&
224
224
  isfinite(props.topOverlayHeight) ? MAX(0, props.topOverlayHeight) : 0;
225
225
  if (_appliedOverlayInset != requestedOverlayInset) {
226
226
  // Keep UIKit's refresh rebound animation in control while it is overscrolled.
@@ -236,7 +236,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
236
236
  }
237
237
  [self emitContentViewportChangeIfNeeded];
238
238
  }
239
- BOOL enabled = props.topScrollEdgeHard && !_recycled && overlay != nil && self.window != nil;
239
+ BOOL enabled = (props.topScrollEdgeEffect != RNCNativeScrollerTopScrollEdgeEffect::None) && !_recycled && overlay != nil && self.window != nil;
240
240
  if (!enabled || _topEdgeInteractionHost != overlay) {
241
241
  if (_topEdgeInteraction != nil) {
242
242
  [_topEdgeInteractionHost removeInteraction:_topEdgeInteraction];
@@ -271,7 +271,8 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
271
271
  [overlay addInteraction:_topEdgeInteraction];
272
272
  _topEdgeInteractionHost = overlay;
273
273
  }
274
- _scrollView.topEdgeEffect.style = UIScrollEdgeEffectStyle.hardStyle;
274
+ _scrollView.topEdgeEffect.style = props.topScrollEdgeEffect == RNCNativeScrollerTopScrollEdgeEffect::Soft
275
+ ? UIScrollEdgeEffectStyle.softStyle : UIScrollEdgeEffectStyle.hardStyle;
275
276
  }
276
277
  }
277
278
 
@@ -90,9 +90,17 @@ using namespace facebook::react;
90
90
  UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.pager.bounds];
91
91
  [self.pager addSubview:scrollView];
92
92
  UIScrollEdgeEffectStyle *originalStyle = scrollView.topEdgeEffect.style;
93
- [self.pager setValue:@YES forKey:@"topScrollEdgeHard"];
93
+ [self.pager setValue:@YES forKey:@"topScrollEdgeEnabled"];
94
+ [self.pager setValue:@(static_cast<NSInteger>(RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard))
95
+ forKey:@"topScrollEdgeEffect"];
94
96
  [self.pager setValue:@123 forKey:@"externalTopOverlayTag"];
95
97
  [self.pager setValue:@64 forKey:@"externalTopOverlayHeight"];
98
+ UIView *container = [self.pager valueForKey:@"containerView"];
99
+ container.frame = self.pager.bounds;
100
+ UIView *overlayHost = [self.pager valueForKey:@"topOverlayHostView"];
101
+ overlayHost.frame = CGRectMake(0, 0, 320, 64);
102
+ overlayHost.hidden = NO;
103
+ self.pager.backgroundColor = UIColor.redColor;
96
104
  [self.pager updateTopScrollEdgeForScrollView:scrollView];
97
105
  XCTAssertEqual([self.pager valueForKey:@"topEdgeScrollView"], scrollView);
98
106
  UIScrollEdgeElementContainerInteraction *interaction =
@@ -100,6 +108,23 @@ using namespace facebook::react;
100
108
  XCTAssertEqual(interaction.scrollView, scrollView);
101
109
  UIView *host = [self.pager valueForKey:@"topEdgeInteractionHost"];
102
110
  XCTAssertTrue([host.interactions containsObject:interaction]);
111
+ XCTAssertEqual(scrollView.topEdgeEffect.style, UIScrollEdgeEffectStyle.hardStyle);
112
+ CALayer *cover = [self.pager valueForKey:@"topEdgeSeparatorCover"];
113
+ XCTAssertEqual(cover.superlayer, container.layer);
114
+ XCTAssertEqualWithAccuracy(CGRectGetMaxY(cover.frame), 64, 0.001);
115
+ XCTAssertEqualWithAccuracy(CGRectGetHeight(cover.frame), 1.0 / window.screen.scale, 0.001);
116
+ XCTAssertTrue(CGColorEqualToColor(cover.backgroundColor, UIColor.redColor.CGColor));
117
+ UIEdgeInsets inset = scrollView.contentInset;
118
+ CGPoint offset = scrollView.contentOffset;
119
+ [self.pager setValue:@(static_cast<NSInteger>(RNCCollapsiblePagerViewTopScrollEdgeEffect::Soft))
120
+ forKey:@"topScrollEdgeEffect"];
121
+ [self.pager updateTopScrollEdgeForScrollView:scrollView];
122
+ XCTAssertEqual(scrollView.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
123
+ XCTAssertNil([self.pager valueForKey:@"topEdgeSeparatorCover"]);
124
+ XCTAssertNil(cover.superlayer);
125
+ XCTAssertEqual([self.pager valueForKey:@"topEdgeInteraction"], interaction);
126
+ XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(scrollView.contentInset, inset));
127
+ XCTAssertTrue(CGPointEqualToPoint(scrollView.contentOffset, offset));
103
128
 
104
129
  RNCPagerViewComponentView *outer = [RNCPagerViewComponentView new];
105
130
  [self.pager setValue:outer forKey:@"externalOuterPager"];
@@ -69,6 +69,45 @@ using namespace facebook::react;
69
69
  XCTAssertFalse([self.scroller.scrollView isKindOfClass:NSClassFromString(@"RCTEnhancedScrollView")]);
70
70
  }
71
71
 
72
+ - (void)testScrollEdgeStyleSwitchPreservesOverlayInsetAndRestoresOriginalStyle
73
+ {
74
+ if (@available(iOS 26.0, *)) {
75
+ UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
76
+ UIView *root = [[UIView alloc] initWithFrame:window.bounds];
77
+ [window addSubview:root];
78
+ [root addSubview:self.scroller];
79
+ RCTViewComponentView *overlay = [RCTViewComponentView new];
80
+ overlay.frame = CGRectMake(0, 0, 320, 64);
81
+ [root addSubview:overlay];
82
+ UIScrollEdgeEffectStyle *original = self.scroller.scrollView.topEdgeEffect.style;
83
+ auto props = std::make_shared<RNCNativeScrollerProps>();
84
+ props->contentHeight = 1500;
85
+ props->topOverlayHeight = 64;
86
+ props->topScrollEdgeEffect = RNCNativeScrollerTopScrollEdgeEffect::Hard;
87
+ [self.scroller updateProps:props oldProps:nullptr];
88
+ XCTAssertEqual(self.scroller.scrollView.topEdgeEffect.style, UIScrollEdgeEffectStyle.hardStyle);
89
+ XCTAssertEqualWithAccuracy(self.scroller.scrollView.contentInset.top, 64, 0.01);
90
+ CGPoint offset = self.scroller.scrollView.contentOffset;
91
+ auto soft = std::make_shared<RNCNativeScrollerProps>();
92
+ soft->contentHeight = 1500;
93
+ soft->topOverlayHeight = 64;
94
+ soft->topScrollEdgeEffect = RNCNativeScrollerTopScrollEdgeEffect::Soft;
95
+ [self.scroller updateProps:soft oldProps:props];
96
+ XCTAssertEqual(self.scroller.scrollView.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
97
+ XCTAssertEqualWithAccuracy(self.scroller.scrollView.contentInset.top, 64, 0.01);
98
+ XCTAssertTrue(CGPointEqualToPoint(self.scroller.scrollView.contentOffset, offset));
99
+ auto disabled = std::make_shared<RNCNativeScrollerProps>();
100
+ disabled->contentHeight = 1500;
101
+ disabled->topScrollEdgeEffect = RNCNativeScrollerTopScrollEdgeEffect::None;
102
+ [self.scroller updateProps:disabled oldProps:soft];
103
+ XCTAssertEqual(self.scroller.scrollView.topEdgeEffect.style, original);
104
+ XCTAssertEqualWithAccuracy(self.scroller.scrollView.contentInset.top, 0, 0.01);
105
+ [self.scroller removeFromSuperview];
106
+ } else {
107
+ XCTSkip(@"Scroll-edge styles require iOS 26 or newer");
108
+ }
109
+ }
110
+
72
111
  - (void)testShrinkClampsOnlyUnreachableUpperOffset
73
112
  {
74
113
  auto props = std::make_shared<RNCNativeScrollerProps>();
@@ -217,8 +217,8 @@ export class CollapsiblePagerView extends React.PureComponent {
217
217
  } = this.props;
218
218
  const pages = this.pages();
219
219
  const supportsTopScrollEdge = Platform.OS === "ios" && parseInt(String(Platform.Version), 10) >= 26;
220
- const hardScrollEdge = supportsTopScrollEdge && topScrollEdgeEffect === "hard";
221
- const hasTopOverlay = hardScrollEdge && topOverlay != null;
220
+ const scrollEdgeEnabled = supportsTopScrollEdge && (topScrollEdgeEffect === "hard" || topScrollEdgeEffect === "soft");
221
+ const hasTopOverlay = scrollEdgeEnabled && topOverlay != null;
222
222
  const pageKeys = pages.map(pageKey);
223
223
  const retained = new Set(this.mountedPages(pages.length));
224
224
  const deducedLayoutDirection = !layoutDirection || layoutDirection === "locale" ? I18nManager.isRTL ? "rtl" : "ltr" : layoutDirection;
@@ -243,8 +243,8 @@ export class CollapsiblePagerView extends React.PureComponent {
243
243
  offscreenPageLimit: Math.max(1, offscreenPageLimit ?? 1),
244
244
  headerHeight: Math.max(0, Math.round(headerHeight)),
245
245
  stickyHeaderHeight: Math.max(0, Math.round(stickyHeaderHeight)),
246
- topScrollEdgeHard: hardScrollEdge,
247
- topOverlayColorScheme: hardScrollEdge ? topOverlayColorScheme : undefined,
246
+ topScrollEdgeEffect: scrollEdgeEnabled ? topScrollEdgeEffect : "none",
247
+ topOverlayColorScheme: scrollEdgeEnabled ? topOverlayColorScheme : undefined,
248
248
  topOverlayHeight: hasTopOverlay ? this.state.topOverlayHeight : 0,
249
249
  externalTopOverlayTag: supportsTopScrollEdge && !hasTopOverlay ? externalTopOverlayTag ?? 0 : 0,
250
250
  nativeSmoothHeaderScrollEnabled: nativeSmoothHeaderScrollEnabled,
@@ -59,7 +59,7 @@ export interface NativeProps extends ViewProps {
59
59
  nativeSmoothHeaderScrollEnabled?: WithDefault<boolean, false>;
60
60
  headerHeight?: Int32;
61
61
  stickyHeaderHeight?: Int32;
62
- topScrollEdgeHard?: WithDefault<boolean, false>;
62
+ topScrollEdgeEffect?: WithDefault<"none" | "hard" | "soft", "none">;
63
63
  topOverlayColorScheme?: string;
64
64
  topOverlayHeight?: WithDefault<Double, 0>;
65
65
  externalTopOverlayTag?: WithDefault<Int32, 0>;
@@ -59,7 +59,7 @@ export const NativeScroller = /*#__PURE__*/React.forwardRef(function NativeScrol
59
59
  height: 0
60
60
  });
61
61
  const [topOverlayHeight, setTopOverlayHeight] = React.useState(0);
62
- const hasTopOverlay = Platform.OS === "ios" && parseInt(String(Platform.Version), 10) >= 26 && topScrollEdgeEffect === "hard" && topOverlay != null;
62
+ const hasTopOverlay = Platform.OS === "ios" && parseInt(String(Platform.Version), 10) >= 26 && (topScrollEdgeEffect === "hard" || topScrollEdgeEffect === "soft") && topOverlay != null;
63
63
  const [refreshEpoch, requestRefreshSync] = React.useReducer(value => value + 1, 0);
64
64
  const contentSizeRef = React.useRef(contentSize);
65
65
  const scrolledSinceGrant = React.useRef(false);
@@ -114,7 +114,7 @@ export const NativeScroller = /*#__PURE__*/React.forwardRef(function NativeScrol
114
114
  contentInsetRight: contentInset?.right ?? 0,
115
115
  contentInsetBottom: contentInset?.bottom ?? 0,
116
116
  contentInsetLeft: contentInset?.left ?? 0,
117
- topScrollEdgeHard: hasTopOverlay,
117
+ topScrollEdgeEffect: hasTopOverlay ? topScrollEdgeEffect : "none",
118
118
  topOverlayHeight: hasTopOverlay ? topOverlayHeight : 0,
119
119
  decelerationRate: rate,
120
120
  keyboardShouldPersistTaps: persistTaps,
@@ -47,7 +47,7 @@ export interface NativeProps extends ViewProps {
47
47
  contentInsetRight?: WithDefault<Double, 0>;
48
48
  contentInsetBottom?: WithDefault<Double, 0>;
49
49
  contentInsetLeft?: WithDefault<Double, 0>;
50
- topScrollEdgeHard?: WithDefault<boolean, false>;
50
+ topScrollEdgeEffect?: WithDefault<"none" | "hard" | "soft", "none">;
51
51
  topOverlayHeight?: WithDefault<Double, 0>;
52
52
  refreshEnabled?: WithDefault<boolean, false>;
53
53
  refreshing?: WithDefault<boolean, false>;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+
3
+ export let ScrollEdgeEffect = /*#__PURE__*/function (ScrollEdgeEffect) {
4
+ ScrollEdgeEffect["Hard"] = "hard";
5
+ ScrollEdgeEffect["Soft"] = "soft";
6
+ return ScrollEdgeEffect;
7
+ }({});
8
+ //# sourceMappingURL=ScrollEdgeEffect.js.map
@@ -5,4 +5,5 @@ export default PagerView;
5
5
  export * from "./usePagerView.js";
6
6
  export * from './CollapsiblePagerView';
7
7
  export * from './NativeScroller';
8
+ export { ScrollEdgeEffect } from "./ScrollEdgeEffect.js";
8
9
  //# sourceMappingURL=index.js.map
@@ -4,4 +4,5 @@ export { PagerView as default, PagerView } from "./PagerView.web.js";
4
4
  export * from "./usePagerView.js";
5
5
  export * from "./CollapsiblePagerView.web.js";
6
6
  export * from "./NativeScroller.web.js";
7
+ export { ScrollEdgeEffect } from "./ScrollEdgeEffect.js";
7
8
  //# sourceMappingURL=index.web.js.map
@@ -1,3 +1,4 @@
1
+ import type { ScrollEdgeEffect } from "./ScrollEdgeEffect";
1
2
  import React from "react";
2
3
  import type * as ReactNative from "react-native";
3
4
  import { type NativeProps, type OnCollapsibleStateChangedEventData, type OnNativeTabPressEventData } from "./CollapsiblePagerViewNativeComponent";
@@ -55,7 +56,7 @@ export type CollapsiblePagerNativeSubHeaderConfig = Readonly<{
55
56
  }>;
56
57
  export type CollapsiblePagerViewOnNativeTabPressEvent = ReactNative.NativeSyntheticEvent<OnNativeTabPressEventData>;
57
58
  export type CollapsiblePagerViewOnNativeSubHeaderPressEvent = ReactNative.NativeSyntheticEvent<OnNativeTabPressEventData>;
58
- export interface CollapsiblePagerViewProps extends Omit<NativeProps, "children" | "headerHeight" | "stickyHeaderHeight" | "topScrollEdgeHard" | "topOverlayHeight" | "externalTopOverlayTag" | "pageKeys" | "retainedPages" | "nativeTabBarItems" | "nativeTabBarHeight" | "nativeTabBarContentPaddingHorizontal" | "nativeTabBarItemSpacing" | "nativeTabBarFontSize" | "nativeTabBarFontFamily" | "nativeTabBarBackgroundColor" | "nativeTabBarActiveTextColor" | "nativeTabBarInactiveTextColor" | "nativeTabBarIndicatorColor" | "nativeTabBarIndicatorHeight" | "nativeTabBarIndicatorBottom" | "nativeSubHeaderConfig" | "nativeSubHeaderSelectedBackgroundColor" | "onNativeTabPress" | "onNativeSubHeaderPress" | "layoutDirection"> {
59
+ export interface CollapsiblePagerViewProps extends Omit<NativeProps, "children" | "headerHeight" | "stickyHeaderHeight" | "topScrollEdgeEffect" | "topOverlayHeight" | "externalTopOverlayTag" | "pageKeys" | "retainedPages" | "nativeTabBarItems" | "nativeTabBarHeight" | "nativeTabBarContentPaddingHorizontal" | "nativeTabBarItemSpacing" | "nativeTabBarFontSize" | "nativeTabBarFontFamily" | "nativeTabBarBackgroundColor" | "nativeTabBarActiveTextColor" | "nativeTabBarInactiveTextColor" | "nativeTabBarIndicatorColor" | "nativeTabBarIndicatorHeight" | "nativeTabBarIndicatorBottom" | "nativeSubHeaderConfig" | "nativeSubHeaderSelectedBackgroundColor" | "onNativeTabPress" | "onNativeSubHeaderPress" | "layoutDirection"> {
59
60
  /** Collapsible content rendered above the sticky bar. */
60
61
  header: React.ReactNode;
61
62
  /** Bar which remains pinned after the header has collapsed. */
@@ -68,8 +69,8 @@ export interface CollapsiblePagerViewProps extends Omit<NativeProps, "children"
68
69
  topOverlay?: React.ReactNode;
69
70
  /** Existing fixed header shared with a parent pager on iOS 26 and newer. */
70
71
  externalTopOverlayTag?: number;
71
- /** Enables UIKit's hard top scroll-edge effect on iOS 26 and newer. */
72
- topScrollEdgeEffect?: "hard";
72
+ /** Selects UIKit's top scroll-edge effect on iOS 26 and newer. */
73
+ topScrollEdgeEffect?: `${ScrollEdgeEffect}`;
73
74
  /** Effective appearance of the fixed overlay or pinned sticky bar. */
74
75
  topOverlayColorScheme?: "light" | "dark";
75
76
  /**
@@ -1,3 +1,4 @@
1
+ import type { ScrollEdgeEffect } from "./ScrollEdgeEffect";
1
2
  import React from "react";
2
3
  import type * as ReactNative from "react-native";
3
4
  export type OnPageScrollEventData = Readonly<{
@@ -40,7 +41,7 @@ export interface CollapsiblePagerViewProps extends Omit<WebCollapsiblePagerNativ
40
41
  headerHeight: number;
41
42
  stickyHeaderHeight: number;
42
43
  topOverlay?: React.ReactNode;
43
- topScrollEdgeEffect?: "hard";
44
+ topScrollEdgeEffect?: `${ScrollEdgeEffect}`;
44
45
  topOverlayColorScheme?: "light" | "dark";
45
46
  externalTopOverlayTag?: number;
46
47
  pageRetentionDistance?: number;
@@ -42,7 +42,7 @@ export interface NativeProps extends ViewProps {
42
42
  nativeSmoothHeaderScrollEnabled?: WithDefault<boolean, false>;
43
43
  headerHeight?: Int32;
44
44
  stickyHeaderHeight?: Int32;
45
- topScrollEdgeHard?: WithDefault<boolean, false>;
45
+ topScrollEdgeEffect?: WithDefault<"none" | "hard" | "soft", "none">;
46
46
  topOverlayColorScheme?: string;
47
47
  topOverlayHeight?: WithDefault<Double, 0>;
48
48
  externalTopOverlayTag?: WithDefault<Int32, 0>;
@@ -1,14 +1,15 @@
1
1
  import * as React from "react";
2
2
  import { type NativeMethods, type RefreshControlProps, type ScrollView, type ScrollViewProps, type ViewProps } from "react-native";
3
3
  import NativeScrollerHost from "./NativeScrollerNativeComponent";
4
+ import type { ScrollEdgeEffect } from "./ScrollEdgeEffect";
4
5
  export declare const NATIVE_SCROLLER_NATIVE_ID_PREFIX = "rnc-collapsible-pager-native-scroller";
5
6
  /** Supported vertical subset; horizontal, snapping, zoom and sticky rows are not supported. */
6
7
  export interface NativeScrollerProps extends ViewProps, Pick<ScrollViewProps, "contentContainerStyle" | "onContentSizeChange" | "scrollEnabled" | "nestedScrollEnabled" | "showsVerticalScrollIndicator" | "showsHorizontalScrollIndicator" | "bounces" | "alwaysBounceVertical" | "contentInset" | "keyboardDismissMode" | "keyboardShouldPersistTaps" | "scrollEventThrottle" | "decelerationRate" | "onScroll" | "onScrollBeginDrag" | "onScrollEndDrag" | "onMomentumScrollBegin" | "onMomentumScrollEnd"> {
7
8
  pagerScrollKey?: string;
8
9
  /** Fixed overlay above scrolling content on iOS 26 and newer. */
9
10
  topOverlay?: React.ReactNode;
10
- /** Enables UIKit's hard top scroll-edge effect on iOS 26 and newer. */
11
- topScrollEdgeEffect?: "hard";
11
+ /** Selects UIKit's top scroll-edge effect on iOS 26 and newer. */
12
+ topScrollEdgeEffect?: `${ScrollEdgeEffect}`;
12
13
  /** iOS only. Android retains AndroidX's native fling physics. */
13
14
  decelerationRate?: ScrollViewProps["decelerationRate"];
14
15
  /** Standard RefreshControl props are mapped to the independently owned native controller. */
@@ -47,7 +47,7 @@ export interface NativeProps extends ViewProps {
47
47
  contentInsetRight?: WithDefault<Double, 0>;
48
48
  contentInsetBottom?: WithDefault<Double, 0>;
49
49
  contentInsetLeft?: WithDefault<Double, 0>;
50
- topScrollEdgeHard?: WithDefault<boolean, false>;
50
+ topScrollEdgeEffect?: WithDefault<"none" | "hard" | "soft", "none">;
51
51
  topOverlayHeight?: WithDefault<Double, 0>;
52
52
  refreshEnabled?: WithDefault<boolean, false>;
53
53
  refreshing?: WithDefault<boolean, false>;
@@ -0,0 +1,5 @@
1
+ export declare enum ScrollEdgeEffect {
2
+ Hard = "hard",
3
+ Soft = "soft"
4
+ }
5
+ //# sourceMappingURL=ScrollEdgeEffect.d.ts.map
@@ -9,4 +9,5 @@ export type { PagerViewOnPageScrollEventData, PagerViewOnPageSelectedEventData,
9
9
  export type PagerViewOnPageScrollEvent = ReactNative.NativeSyntheticEvent<PagerViewOnPageScrollEventData>;
10
10
  export type PagerViewOnPageSelectedEvent = ReactNative.NativeSyntheticEvent<PagerViewOnPageSelectedEventData>;
11
11
  export type PageScrollStateChangedNativeEvent = ReactNative.NativeSyntheticEvent<PageScrollStateChangedNativeEventData>;
12
+ export { ScrollEdgeEffect } from './ScrollEdgeEffect';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -3,4 +3,5 @@ export type { PagerViewProps, PagerViewOnPageScrollEvent, PagerViewOnPageScrollE
3
3
  export * from "./usePagerView";
4
4
  export * from "./CollapsiblePagerView.web";
5
5
  export * from "./NativeScroller.web";
6
+ export { ScrollEdgeEffect } from "./ScrollEdgeEffect";
6
7
  //# sourceMappingURL=index.web.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-pager-view",
3
- "version": "3.0.180",
3
+ "version": "6.7.0-2",
4
4
  "description": "React Native wrapper for Android and iOS ViewPager",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
@@ -72,7 +72,7 @@
72
72
  "typescript": "^5.9.2"
73
73
  },
74
74
  "peerDependencies": {
75
- "@onekeyfe/react-native-native-logger": "3.0.180",
75
+ "@onekeyfe/react-native-native-logger": "6.7.0-2",
76
76
  "react": "*",
77
77
  "react-native": "*"
78
78
  },
@@ -1,3 +1,4 @@
1
+ import type { ScrollEdgeEffect } from "./ScrollEdgeEffect";
1
2
  import React from "react";
2
3
  import { I18nManager, Platform, StyleSheet, View } from "react-native";
3
4
  import type * as ReactNative from "react-native";
@@ -86,7 +87,7 @@ export interface CollapsiblePagerViewProps
86
87
  | "children"
87
88
  | "headerHeight"
88
89
  | "stickyHeaderHeight"
89
- | "topScrollEdgeHard"
90
+ | "topScrollEdgeEffect"
90
91
  | "topOverlayHeight"
91
92
  | "externalTopOverlayTag"
92
93
  | "pageKeys"
@@ -121,8 +122,8 @@ export interface CollapsiblePagerViewProps
121
122
  topOverlay?: React.ReactNode;
122
123
  /** Existing fixed header shared with a parent pager on iOS 26 and newer. */
123
124
  externalTopOverlayTag?: number;
124
- /** Enables UIKit's hard top scroll-edge effect on iOS 26 and newer. */
125
- topScrollEdgeEffect?: "hard";
125
+ /** Selects UIKit's top scroll-edge effect on iOS 26 and newer. */
126
+ topScrollEdgeEffect?: `${ScrollEdgeEffect}`;
126
127
  /** Effective appearance of the fixed overlay or pinned sticky bar. */
127
128
  topOverlayColorScheme?: "light" | "dark";
128
129
  /**
@@ -493,8 +494,10 @@ export class CollapsiblePagerView extends React.PureComponent<
493
494
  const pages = this.pages();
494
495
  const supportsTopScrollEdge =
495
496
  Platform.OS === "ios" && parseInt(String(Platform.Version), 10) >= 26;
496
- const hardScrollEdge = supportsTopScrollEdge && topScrollEdgeEffect === "hard";
497
- const hasTopOverlay = hardScrollEdge && topOverlay != null;
497
+ const scrollEdgeEnabled =
498
+ supportsTopScrollEdge &&
499
+ (topScrollEdgeEffect === "hard" || topScrollEdgeEffect === "soft");
500
+ const hasTopOverlay = scrollEdgeEnabled && topOverlay != null;
498
501
  const pageKeys = pages.map(pageKey);
499
502
  const retained = new Set(this.mountedPages(pages.length));
500
503
  const deducedLayoutDirection =
@@ -535,8 +538,10 @@ export class CollapsiblePagerView extends React.PureComponent<
535
538
  offscreenPageLimit={Math.max(1, offscreenPageLimit ?? 1)}
536
539
  headerHeight={Math.max(0, Math.round(headerHeight))}
537
540
  stickyHeaderHeight={Math.max(0, Math.round(stickyHeaderHeight))}
538
- topScrollEdgeHard={hardScrollEdge}
539
- topOverlayColorScheme={hardScrollEdge ? topOverlayColorScheme : undefined}
541
+ topScrollEdgeEffect={scrollEdgeEnabled ? topScrollEdgeEffect : "none"}
542
+ topOverlayColorScheme={
543
+ scrollEdgeEnabled ? topOverlayColorScheme : undefined
544
+ }
540
545
  topOverlayHeight={hasTopOverlay ? this.state.topOverlayHeight : 0}
541
546
  externalTopOverlayTag={
542
547
  supportsTopScrollEdge && !hasTopOverlay
@@ -1,3 +1,4 @@
1
+ import type { ScrollEdgeEffect } from "./ScrollEdgeEffect";
1
2
  import React from "react";
2
3
  import { I18nManager, StyleSheet, View } from "react-native";
3
4
  import type * as ReactNative from "react-native";
@@ -67,7 +68,7 @@ export interface CollapsiblePagerViewProps
67
68
  headerHeight: number;
68
69
  stickyHeaderHeight: number;
69
70
  topOverlay?: React.ReactNode;
70
- topScrollEdgeEffect?: "hard";
71
+ topScrollEdgeEffect?: `${ScrollEdgeEffect}`;
71
72
  topOverlayColorScheme?: "light" | "dark";
72
73
  externalTopOverlayTag?: number;
73
74
  pageRetentionDistance?: number;
@@ -59,7 +59,7 @@ export interface NativeProps extends ViewProps {
59
59
  nativeSmoothHeaderScrollEnabled?: WithDefault<boolean, false>;
60
60
  headerHeight?: Int32;
61
61
  stickyHeaderHeight?: Int32;
62
- topScrollEdgeHard?: WithDefault<boolean, false>;
62
+ topScrollEdgeEffect?: WithDefault<"none" | "hard" | "soft", "none">;
63
63
  topOverlayColorScheme?: string;
64
64
  topOverlayHeight?: WithDefault<Double, 0>;
65
65
  externalTopOverlayTag?: WithDefault<Int32, 0>;
@@ -15,6 +15,7 @@ import {
15
15
  } from "react-native";
16
16
 
17
17
  import NativeScrollerHost, { Commands } from "./NativeScrollerNativeComponent";
18
+ import type { ScrollEdgeEffect } from "./ScrollEdgeEffect";
18
19
 
19
20
  export const NATIVE_SCROLLER_NATIVE_ID_PREFIX =
20
21
  "rnc-collapsible-pager-native-scroller";
@@ -46,8 +47,8 @@ export interface NativeScrollerProps
46
47
  pagerScrollKey?: string;
47
48
  /** Fixed overlay above scrolling content on iOS 26 and newer. */
48
49
  topOverlay?: React.ReactNode;
49
- /** Enables UIKit's hard top scroll-edge effect on iOS 26 and newer. */
50
- topScrollEdgeEffect?: "hard";
50
+ /** Selects UIKit's top scroll-edge effect on iOS 26 and newer. */
51
+ topScrollEdgeEffect?: `${ScrollEdgeEffect}`;
51
52
  /** iOS only. Android retains AndroidX's native fling physics. */
52
53
  decelerationRate?: ScrollViewProps["decelerationRate"];
53
54
  /** Standard RefreshControl props are mapped to the independently owned native controller. */
@@ -118,7 +119,8 @@ export const NativeScroller = React.forwardRef<
118
119
  const [topOverlayHeight, setTopOverlayHeight] = React.useState(0);
119
120
  const hasTopOverlay =
120
121
  Platform.OS === "ios" && parseInt(String(Platform.Version), 10) >= 26 &&
121
- topScrollEdgeEffect === "hard" && topOverlay != null;
122
+ (topScrollEdgeEffect === "hard" || topScrollEdgeEffect === "soft") &&
123
+ topOverlay != null;
122
124
  const [refreshEpoch, requestRefreshSync] = React.useReducer(
123
125
  (value: number) => value + 1,
124
126
  0
@@ -212,7 +214,7 @@ export const NativeScroller = React.forwardRef<
212
214
  contentInsetRight={contentInset?.right ?? 0}
213
215
  contentInsetBottom={contentInset?.bottom ?? 0}
214
216
  contentInsetLeft={contentInset?.left ?? 0}
215
- topScrollEdgeHard={hasTopOverlay}
217
+ topScrollEdgeEffect={hasTopOverlay ? topScrollEdgeEffect : "none"}
216
218
  topOverlayHeight={hasTopOverlay ? topOverlayHeight : 0}
217
219
  decelerationRate={rate}
218
220
  keyboardShouldPersistTaps={persistTaps}
@@ -47,7 +47,7 @@ export interface NativeProps extends ViewProps {
47
47
  contentInsetRight?: WithDefault<Double, 0>;
48
48
  contentInsetBottom?: WithDefault<Double, 0>;
49
49
  contentInsetLeft?: WithDefault<Double, 0>;
50
- topScrollEdgeHard?: WithDefault<boolean, false>;
50
+ topScrollEdgeEffect?: WithDefault<"none" | "hard" | "soft", "none">;
51
51
  topOverlayHeight?: WithDefault<Double, 0>;
52
52
  refreshEnabled?: WithDefault<boolean, false>;
53
53
  refreshing?: WithDefault<boolean, false>;
@@ -0,0 +1,4 @@
1
+ export enum ScrollEdgeEffect {
2
+ Hard = "hard",
3
+ Soft = "soft",
4
+ }
package/src/index.tsx CHANGED
@@ -27,3 +27,5 @@ export type PagerViewOnPageSelectedEvent =
27
27
 
28
28
  export type PageScrollStateChangedNativeEvent =
29
29
  ReactNative.NativeSyntheticEvent<PageScrollStateChangedNativeEventData>;
30
+
31
+ export { ScrollEdgeEffect } from './ScrollEdgeEffect';
package/src/index.web.tsx CHANGED
@@ -11,3 +11,5 @@ export type {
11
11
  export * from "./usePagerView";
12
12
  export * from "./CollapsiblePagerView.web";
13
13
  export * from "./NativeScroller.web";
14
+
15
+ export { ScrollEdgeEffect } from "./ScrollEdgeEffect";