@onekeyfe/react-native-pager-view 6.7.0-1 → 6.7.0-3

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 (38) hide show
  1. package/README.md +9 -0
  2. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +2 -2
  3. package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +2 -2
  4. package/docs/SPEC.md +77 -4
  5. package/ios/RNCCollapsiblePagerViewComponentView.mm +84 -21
  6. package/ios/RNCNativeScrollerComponentView.mm +6 -5
  7. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +26 -1
  8. package/ios/tests/RNCNativeScrollerTests.mm +39 -0
  9. package/lib/module/CollapsiblePagerView.js +4 -4
  10. package/lib/module/CollapsiblePagerView.web.js +428 -37
  11. package/lib/module/CollapsiblePagerViewNativeComponent.ts +1 -1
  12. package/lib/module/NativeScroller.js +2 -2
  13. package/lib/module/NativeScroller.web.js +16 -3
  14. package/lib/module/NativeScrollerNativeComponent.ts +1 -1
  15. package/lib/module/ScrollEdgeEffect.js +8 -0
  16. package/lib/module/WebCollapsiblePagerPage.js +182 -0
  17. package/lib/module/index.js +3 -0
  18. package/lib/module/index.web.js +3 -0
  19. package/lib/typescript/src/CollapsiblePagerView.d.ts +4 -3
  20. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +66 -4
  21. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +1 -1
  22. package/lib/typescript/src/NativeScroller.d.ts +3 -2
  23. package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +1 -1
  24. package/lib/typescript/src/ScrollEdgeEffect.d.ts +5 -0
  25. package/lib/typescript/src/WebCollapsiblePagerPage.d.ts +42 -0
  26. package/lib/typescript/src/index.d.ts +5 -0
  27. package/lib/typescript/src/index.web.d.ts +5 -0
  28. package/package.json +2 -2
  29. package/src/CollapsiblePagerView.tsx +12 -7
  30. package/src/CollapsiblePagerView.web.tsx +640 -50
  31. package/src/CollapsiblePagerViewNativeComponent.ts +1 -1
  32. package/src/NativeScroller.tsx +6 -4
  33. package/src/NativeScroller.web.tsx +24 -2
  34. package/src/NativeScrollerNativeComponent.ts +1 -1
  35. package/src/ScrollEdgeEffect.ts +4 -0
  36. package/src/WebCollapsiblePagerPage.tsx +258 -0
  37. package/src/index.tsx +6 -0
  38. package/src/index.web.tsx +6 -0
package/README.md CHANGED
@@ -67,6 +67,15 @@ Web scroll child can opt in by exposing a DOM element with
67
67
  `data-collapsible-pager-scroll`. CSS scroll timelines drive header/sticky
68
68
  movement, so React is not updated on each vertical scroll frame.
69
69
 
70
+ On Web, `WebCollapsiblePagerView` is an explicit root export of the existing Web
71
+ pager. It measures Header and sticky content when their heights are omitted.
72
+ `isActive` describes route visibility; the pager preserves a pending scroll
73
+ target across a short or temporarily hidden page and retries when real content
74
+ geometry becomes ready. `useWebCollapsiblePagerPage()` exposes the selected
75
+ page's scroll owner, visible metrics, content registration and pending target
76
+ to descendants. These Web-only APIs require a DOM when mounted. Native callers
77
+ continue to use `CollapsiblePagerView`.
78
+
70
79
  On iOS and Android, `nativeTabBar` renders the tab bar natively and
71
80
  `onNativeTabPress` reports presses. By default, adjacent tab presses animate and
72
81
  non-adjacent presses jump directly to the target without passing intermediate
@@ -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
@@ -814,3 +841,49 @@ The native Home Spotlight adapter consumes this snapshot in the main UI runtime
814
841
  Removing a page or clearing the page collection MUST cancel the active horizontal transition and its temporary display snapshots before notifying the adapter. Cancellation MUST NOT rewrite the shared Header collapse or surviving pages' saved body reading memory. The previous observed owner MUST be detached without committing an offset under a shifted page key.
815
842
 
816
843
  An empty adapter MUST reject transition starts and transition completion without coercing an index into an empty range. The selected index uses zero as an empty-state sentinel; nonempty mutations clamp it to the remaining valid range. Readding pages MUST allow a fresh selection and MUST NOT resume a removed transition source. Conformance requires actual Android active-transition → zero-pages → readd/selection regression and remaining-page mutation checks; structural checks and compilation alone do not establish runtime behavior.
844
+
845
+
846
+ ## Web parent-scroll owner identity
847
+
848
+ Web Header and sticky overlays use the named scroll timeline only when the
849
+ browser supports every CSS property needed for that linkage. Otherwise, the
850
+ active scroll owner's offset drives the same clamped Header collapse in JS:
851
+ `C = min(max(scrollTop, 0), max(headerHeight, 0))`. The fallback keeps the
852
+ existing page owner, insets, body offset memory, and sticky anchor. It updates
853
+ when scrolling, selecting or restoring a page, replacing an owner, or changing
854
+ Header height, and releases its listener when that owner detaches. An inactive
855
+ page cannot move the visible Header. This is a Web-only compatibility path; no
856
+ native behavior or public prop changes.
857
+ When an overlay forwards a vertical wheel or touch scroll to the active owner,
858
+ the owner receives the same internal user-input notification used by route
859
+ restoration to cancel an obsolete pending offset.
860
+
861
+ On Web, an explicitly marked NativeScroller is selected before an implicit NativeList viewport. There is one vertical owner per page; callers using parent-owned NativeList pass that same scroll element as webScrollHost.
862
+
863
+ The existing NativeScroller pagerScrollKey identifies a Web page scroll owner. Changing the key invalidates that page body offset without remounting its React business content. The active owner also resets the shared header collapse and cancels queued old restore/top commands. Inactive owners invalidate their saved body offset without imperative scrolling; selection checks the current identity before restoring. A→B→A identity changes never revive a prior owner offset. Stable keys preserve ordinary page C+B restoration. Current-page top commands continue to affect only the selected page.
864
+
865
+ Identity notifications and DOM owner listeners are internal implementation details, removed when the pager root or owner is replaced/unmounted. No native behavior or public configuration is added.
866
+
867
+ ### Web overlay input and keyed-page updates
868
+
869
+ - Header and sticky slots remain overlays: overflow outside the sticky slot may remain visible without adding its portal height to page insets twice.
870
+ - Vertical wheel or touch input over those overlays transfers only to the active page scroll owner when no nested scrollable consumes it. Horizontal controls, editable inputs, and ordinary button/tap behavior retain their own events. Input originating inside the page owner follows native scrolling once.
871
+ - Reordering or removing keyed pages must save the previously configured key and owner, rather than resolving the old selected index against new children. Caller index synchronization preserves keyed body offsets and shared header collapse.
872
+ - For an explicit NativeScroller, Pager chrome insets apply to the existing ScrollView content element, not its viewport. Insets taller than the available window cannot enlarge the scroll viewport. Existing owner/content padding is preserved and restored separately from Pager insets; direct NativeList/page-host fallback retains its prior layout path.
873
+ - The Pager root clips without becoming a programmatically scrollable overflow box. Browser focus and descendant scrollIntoView cannot add a second horizontal page offset outside the selected-page transform.
874
+
875
+ ## Web page geometry and route restoration
876
+
877
+ Status: Implemented in Web source and focused tests; browser and native runtime acceptance remain separate.
878
+
879
+ `WebCollapsiblePagerView` is the existing Web `CollapsiblePagerView` class under an explicit root export. Its `isActive` prop defaults to true and represents route visibility, independently of the selected page. The Web-only `useWebCollapsiblePagerPage()` hook returns null outside a Web pager and otherwise exposes `isFocused`, `isRouteActive`, `owner`, `content`, optional `contentHeight`, `width`, `height`, content-relative `scrollTop`, `registerContent`, and read-only `pendingRouteScrollTop`. Native `CollapsiblePagerView`, its bridge, and its props are unchanged. The Web component rejects mounting without a DOM; importing the root or Web module alone must not access DOM globals.
880
+
881
+ Web callers may supply finite nonnegative `headerHeight` and `stickyHeaderHeight`. When omitted or invalid (negative, NaN, or infinite), the SDK measures the natural inner Header and sticky content with `ResizeObserver`; explicit valid values take precedence, including zero. An initial unmeasured zero is not ready for route restoration; a measured empty chrome element may legitimately have zero height. Measurement must not use the constrained slot height as its own input. Height changes update the owned insets before route restoration. Existing explicit-height callers retain their layout path.
882
+
883
+ Each page has one SDK-discovered vertical owner: explicit `NativeScroller`, then implicit NativeList viewport, then page host. Registration accepts a connected content element and optional finite nonnegative explicit height; invalid height metadata is ignored. It does not change DOM layout or make a pending target reachable. A later registration of the same element supersedes an earlier one, and stale cleanup cannot remove it. An explicit-height change schedules geometry measurement even if the element border box is unchanged. Metrics use owner client width; `pinnedBottom = ownerRect.top + max(0, headerHeight - owner.scrollTop) + stickyHeaderHeight`; `visibleTop = max(ownerRect.top, pinnedBottom, contentRect.top if present)`; visible height is `max(0, ownerRect.bottom - visibleTop)` and content-relative scroll top is `max(0, pinnedBottom - contentRect.top)`. Unregistered content falls back to the first owner child. Detached content and callbacks from old owners are ignored. One animation-frame measurement per page coalesces scroll, resize, registration, owner configuration, and active-page changes. Disposal releases observers, listeners, frames, and subscriptions.
884
+
885
+ On active-to-inactive transition, the Web pager freezes the selected logical React page key, `pagerScrollKey` identity, actual owner generation, and last valid raw DOM top. Inactive scroll/geometry changes cannot replace the snapshot. A second blur of the same pending identity preserves the original target. On focus, owner, content, chrome, or inset changes, the pager retries without a timer only when the route is active, that exact key/identity/owner remains selected and connected, the viewport has positive height, the pager has configured the expected insets, and real `scrollHeight - clientHeight` reaches the target. Short content keeps the target pending. Restoration clears pending before one instant scroll and synchronizes the existing shared Header C and per-page body B memory. An idempotent Web `restoreScrollPosition()` returns false while inactive or not ready and true when no target remains or it applies; normal callers need no polling.
886
+
887
+ Valid vertical wheel, scroll keys, scrollbar drag, overlay-forwarded input, accepted NativeList commands, and explicit page-top/sticky-top commands cancel pending route intent through the existing SDK takeover path. As in the prior Web app adapter, any owner `pointerdown` or `touchstart`, including a child target, cancels only route pending; this does not newly commit or clear SDK C+B memory. Geometry-only scroll/resize/scrollend cannot cancel intent. Valid same-offset commands cancel it; invalid or stale commands do not. A matching route target takes priority over automatic tab C+B restoration. Selection of a different key, owner identity reset (including A→B→A), genuine DOM owner replacement, or removal cancels pending; a same-key index reorder preserves it. A same-commit blur and Activity/Offscreen effect disconnect captures the current route target before releasing the owner; repeated disconnects preserve an existing pending target. A temporary React ref null or Activity/Offscreen effect disconnect followed by the identical connected owner preserves the logical route target and owner generation while releasing all observers, listeners, frames, and DOM configuration during disconnect. Restoration is unavailable while disconnected; reconnect remeasures automatic chrome before applying pending. A truly removed pager instance cannot transfer its pending target to a new instance. Older queued tab/top callbacks must validate generation before writing after route restoration or cancellation. The app owns navigation focus and business tab names; the SDK never infers them from escaped React child keys.
888
+
889
+ Platform matrix: Web owns this DOM geometry, registration, route pending, and explicit Web export. iOS and Android keep native scrolling, restoration, and page coordination documented above, with no new prop, native bridge call, JS heap sharing, or process resource. The UI main runtime owns Web state; background runtimes do not deserialize it. Source/type tests establish only Web contract implementation, not browser rendering or native device behavior. Acceptance requires real Web/Desktop route blur, short content then growth, user takeover, dynamic chrome, key reset, and Home/Market scroll regressions.
@@ -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,