@onekeyfe/react-native-pager-view 3.0.177 → 3.0.179-alpha.20261015

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.
@@ -33,12 +33,15 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
33
33
  @end
34
34
 
35
35
  @implementation RNCNativeScrollerComponentView {
36
- #if DEBUG
37
- NSUInteger _offsetDiagSequence;
38
- #endif
39
36
  RNCNativeScrollerShadowNode::ConcreteState::Shared _state;
40
37
  RNCNativeVerticalScrollView *_scrollView;
41
38
  UIView<RCTComponentViewProtocol> *_reactContent;
39
+ __weak UIView *_topOverlay;
40
+ CGFloat _appliedOverlayInset;
41
+ UIScrollEdgeElementContainerInteraction *_topEdgeInteraction;
42
+ __weak UIView *_topEdgeInteractionHost;
43
+ UILabel *_topEdgeMarker;
44
+ UIScrollEdgeEffectStyle *_originalTopEdgeStyle;
42
45
  UIRefreshControl *_refreshControl;
43
46
  UIEdgeInsets _callerInset;
44
47
  BOOL _dragActive;
@@ -54,24 +57,6 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
54
57
  NSNumber *_lastContentViewportHeight;
55
58
  }
56
59
 
57
- - (void)offsetDiag:(NSString *)reason
58
- {
59
- #if DEBUG
60
- Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog") ?: NSClassFromString(@"OneKeyLog");
61
- SEL selector = NSSelectorFromString(@"debug::");
62
- if (logClass == nil || ![logClass respondsToSelector:selector]) return;
63
- NSString *message = [NSString stringWithFormat:
64
- @"[HOME-OFFSET-DIAG] scroller phase=%@ seq=%lu wallMs=%.0f mono=%.6f host=%p tag=%ld scroll=%p generation=%lu physicalY=%.4f insetTop=%.4f logicalY=%.4f contentHeight=%.4f boundsHeight=%.4f adjustedBottom=%.4f needsClamp=%d drag=%d decel=%d refresh=%d",
65
- reason, (unsigned long)++_offsetDiagSequence, NSDate.date.timeIntervalSince1970 * 1000, CACurrentMediaTime(),
66
- (__bridge void *)self, (long)self.tag, (__bridge void *)_scrollView, (unsigned long)_generation,
67
- _scrollView.contentOffset.y, _scrollView.contentInset.top, _scrollView.contentOffset.y + _scrollView.contentInset.top,
68
- _scrollView.contentSize.height, _scrollView.bounds.size.height, _scrollView.adjustedContentInset.bottom,
69
- _needsContentClamp, _scrollView.dragging, _scrollView.decelerating, _refreshControl.refreshing];
70
- typedef void (*LogFunction)(id, SEL, NSString *, NSString *);
71
- ((LogFunction)[logClass methodForSelector:selector])(logClass, selector, @"NativeScroller", message);
72
- #endif
73
- }
74
-
75
60
  + (ComponentDescriptorProvider)componentDescriptorProvider
76
61
  {
77
62
  return concreteComponentDescriptorProvider<RNCNativeScrollerComponentDescriptor>();
@@ -200,9 +185,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
200
185
  _needsContentClamp |= size.height < _scrollView.contentSize.height;
201
186
  CGPoint offset = _scrollView.contentOffset;
202
187
  BOOL wasPulling = offset.y < -_scrollView.adjustedContentInset.top;
203
- [self offsetDiag:@"content-size-before"];
204
188
  _scrollView.contentSize = size;
205
- [self offsetDiag:@"content-size-after"];
206
189
  // UIKit can synchronously discard a pull when content becomes empty.
207
190
  // Upper-range shrink correction remains owned by the deferred clamp.
208
191
  if (wasPulling && _scrollView.contentOffset.y != offset.y) {
@@ -212,20 +195,99 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
212
195
  }
213
196
  BOOL refreshEnded = previous.refreshing && !next.refreshing;
214
197
  [super updateProps:props oldProps:oldProps];
198
+ [self updateTopScrollEdge];
199
+ if (next.topScrollEdgeHard && _topOverlay == nil) {
200
+ __weak __typeof__(self) weakSelf = self;
201
+ dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf updateTopScrollEdge]; });
202
+ }
215
203
  if (refreshEnded) [self scheduleContentClamp];
216
204
  [self updateFabricContentOrigin];
217
205
  [self emitContentViewportChangeIfNeeded];
218
206
  }
219
207
 
208
+ - (void)updateTopScrollEdge
209
+ {
210
+ if (@available(iOS 26.0, *)) {
211
+ const auto &props = *std::static_pointer_cast<const RNCNativeScrollerProps>(_props);
212
+ UIView *overlay = nil;
213
+ if (props.topScrollEdgeHard && !_recycled) {
214
+ NSUInteger fabricSiblings = 0;
215
+ for (UIView *candidate in self.superview.subviews) {
216
+ if (![candidate conformsToProtocol:@protocol(RCTComponentViewProtocol)]) continue;
217
+ fabricSiblings++;
218
+ if (candidate != self) overlay = candidate;
219
+ }
220
+ if (fabricSiblings != 2) overlay = nil;
221
+ }
222
+ _topOverlay = overlay;
223
+ CGFloat requestedOverlayInset = props.topScrollEdgeHard && !_recycled && overlay != nil &&
224
+ isfinite(props.topOverlayHeight) ? MAX(0, props.topOverlayHeight) : 0;
225
+ if (_appliedOverlayInset != requestedOverlayInset) {
226
+ // Keep UIKit's refresh rebound animation in control while it is overscrolled.
227
+ BOOL wasReturningFromPull = _scrollView.contentOffset.y < -_scrollView.adjustedContentInset.top - 0.5;
228
+ CGFloat logicalOffset = _scrollView.contentOffset.y + _scrollView.contentInset.top;
229
+ UIEdgeInsets current = _scrollView.contentInset;
230
+ current.top += requestedOverlayInset - _appliedOverlayInset;
231
+ _scrollView.contentInset = current;
232
+ _appliedOverlayInset = requestedOverlayInset;
233
+ if (!wasReturningFromPull) {
234
+ [_scrollView setContentOffset:CGPointMake(_scrollView.contentOffset.x,
235
+ logicalOffset - current.top) animated:NO];
236
+ }
237
+ [self emitContentViewportChangeIfNeeded];
238
+ }
239
+ BOOL enabled = props.topScrollEdgeHard && !_recycled && overlay != nil && self.window != nil;
240
+ if (!enabled || _topEdgeInteractionHost != overlay) {
241
+ if (_topEdgeInteraction != nil) {
242
+ [_topEdgeInteractionHost removeInteraction:_topEdgeInteraction];
243
+ _topEdgeInteraction.scrollView = nil;
244
+ _topEdgeInteraction = nil;
245
+ _topEdgeInteractionHost = nil;
246
+ }
247
+ [_topEdgeMarker removeFromSuperview];
248
+ _topEdgeMarker = nil;
249
+ if (_originalTopEdgeStyle != nil) {
250
+ _scrollView.topEdgeEffect.style = _originalTopEdgeStyle;
251
+ _originalTopEdgeStyle = nil;
252
+ }
253
+ if (!enabled) return;
254
+ }
255
+ if (_topEdgeInteraction == nil) {
256
+ _originalTopEdgeStyle = _scrollView.topEdgeEffect.style;
257
+ // UIKit needs a native element to shape the edge effect around React-drawn content.
258
+ _topEdgeMarker = [UILabel new];
259
+ _topEdgeMarker.frame = overlay.bounds;
260
+ _topEdgeMarker.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
261
+ _topEdgeMarker.text = @" ";
262
+ _topEdgeMarker.textColor = UIColor.clearColor;
263
+ _topEdgeMarker.backgroundColor = UIColor.clearColor;
264
+ _topEdgeMarker.userInteractionEnabled = NO;
265
+ _topEdgeMarker.isAccessibilityElement = NO;
266
+ _topEdgeMarker.accessibilityElementsHidden = YES;
267
+ [overlay addSubview:_topEdgeMarker];
268
+ _topEdgeInteraction = [UIScrollEdgeElementContainerInteraction new];
269
+ _topEdgeInteraction.edge = UIRectEdgeTop;
270
+ _topEdgeInteraction.scrollView = _scrollView;
271
+ [overlay addInteraction:_topEdgeInteraction];
272
+ _topEdgeInteractionHost = overlay;
273
+ }
274
+ _scrollView.topEdgeEffect.style = UIScrollEdgeEffectStyle.hardStyle;
275
+ }
276
+ }
277
+
278
+ - (void)didMoveToSuperview
279
+ {
280
+ [super didMoveToSuperview];
281
+ [self updateTopScrollEdge];
282
+ }
283
+
220
284
  - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
221
285
  oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
222
286
  {
223
- [self offsetDiag:@"layout-before"];
224
287
  CGSize previous = _scrollView.bounds.size;
225
288
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
226
289
  [self updateFabricContentOrigin];
227
290
  [self emitContentViewportChangeIfNeeded];
228
- [self offsetDiag:@"layout-after"];
229
291
  if (!CGSizeEqualToSize(previous, _scrollView.bounds.size)) {
230
292
  _needsContentClamp = YES;
231
293
  [self scheduleContentClamp];
@@ -236,7 +298,8 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
236
298
  {
237
299
  CGFloat stickyHeight = 0;
238
300
  for (NSNumber *height in _pagerStickyHeights.objectEnumerator) stickyHeight += height.doubleValue;
239
- return MAX(0, _scrollView.bounds.size.height - _callerInset.top - _callerInset.bottom - stickyHeight);
301
+ return MAX(0, _scrollView.bounds.size.height - _callerInset.top - _callerInset.bottom
302
+ - _appliedOverlayInset - stickyHeight);
240
303
  }
241
304
 
242
305
  - (void)setPagerStickyHeight:(CGFloat)height forOwner:(id)owner
@@ -288,7 +351,6 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
288
351
  - (void)scheduleContentClamp
289
352
  {
290
353
  if (!_needsContentClamp) return;
291
- [self offsetDiag:@"clamp-scheduled"];
292
354
  NSUInteger generation = _generation;
293
355
  __weak __typeof__(self) weakSelf = self;
294
356
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -300,19 +362,13 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
300
362
 
301
363
  - (void)clampContentOffsetIfNeeded
302
364
  {
303
- [self offsetDiag:@"clamp-enter-or-deferred"];
304
365
  if (!_needsContentClamp || _scrollView.dragging || _scrollView.decelerating ||
305
366
  _refreshControl.refreshing || _scrollView.bounds.size.height <= 0) return;
306
367
  _needsContentClamp = NO;
307
368
  CGFloat maximum = MAX(-_scrollView.adjustedContentInset.top,
308
369
  _scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.adjustedContentInset.bottom);
309
- #if DEBUG
310
- [self offsetDiag:[NSString stringWithFormat:@"clamp-candidate-max=%.4f", maximum]];
311
- #endif
312
370
  if (_scrollView.contentOffset.y > maximum) {
313
- [self offsetDiag:@"clamp-applied-before"];
314
371
  [_scrollView setContentOffset:CGPointMake(_scrollView.contentOffset.x, maximum) animated:NO];
315
- [self offsetDiag:@"clamp-applied-after"];
316
372
  }
317
373
  }
318
374
 
@@ -353,7 +409,6 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
353
409
  - (void)stopScrolling
354
410
  {
355
411
  if (_stopping) return;
356
- [self offsetDiag:@"stop-before"];
357
412
  _stopping = YES;
358
413
  [_scrollView setContentOffset:_scrollView.contentOffset animated:NO];
359
414
  if (_dragActive) {
@@ -365,7 +420,6 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
365
420
  }
366
421
  [self finishMomentum];
367
422
  _stopping = NO;
368
- [self offsetDiag:@"stop-after"];
369
423
  }
370
424
 
371
425
  - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
@@ -435,9 +489,6 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
435
489
  [(id<RNCNativeScrollerInsetCoordinator>)owner nativeScrollerWillReceiveScrollCommand:self];
436
490
  }
437
491
  }
438
- #if DEBUG
439
- [self offsetDiag:[NSString stringWithFormat:@"command-scrollTo-enter-x=%.4f-y=%.4f-animated=%d", x, y, animated]];
440
- #endif
441
492
  [self stopScrolling];
442
493
  UIEdgeInsets inset = _scrollView.adjustedContentInset;
443
494
  CGPoint target = CGPointMake(MAX(-inset.left, MIN(x,
@@ -489,6 +540,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
489
540
  - (void)didMoveToWindow
490
541
  {
491
542
  [super didMoveToWindow];
543
+ [self updateTopScrollEdge];
492
544
  if (self.window == nil) {
493
545
  [self stopScrolling];
494
546
  [_pagerStickyHeights removeAllObjects];
@@ -504,6 +556,9 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
504
556
  {
505
557
  [self stopScrolling];
506
558
  _recycled = YES;
559
+ [self updateTopScrollEdge];
560
+ _topOverlay = nil;
561
+ _appliedOverlayInset = 0;
507
562
  _scrollView.delegate = nil;
508
563
  ++_generation;
509
564
  _state.reset();
@@ -4,8 +4,15 @@
4
4
 
5
5
  NS_ASSUME_NONNULL_BEGIN
6
6
 
7
+ @protocol RNCPagerViewNestedOverlayObserver <NSObject>
8
+ - (void)outerPagerDidUpdateOverlayOwner:(BOOL)active transitioning:(BOOL)transitioning;
9
+ @end
10
+
7
11
  @interface RNCPagerViewComponentView : RCTViewComponentView
8
12
 
13
+ - (void)addNestedOverlayObserver:(id<RNCPagerViewNestedOverlayObserver>)observer;
14
+ - (void)removeNestedOverlayObserver:(id<RNCPagerViewNestedOverlayObserver>)observer;
15
+
9
16
  @end
10
17
 
11
18
  NS_ASSUME_NONNULL_END
@@ -12,6 +12,7 @@
12
12
 
13
13
  using namespace facebook::react;
14
14
 
15
+
15
16
  @interface RNCPagerViewComponentView () <RCTRNCViewPagerViewProtocol, UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate>
16
17
 
17
18
  @property(nonatomic, strong) UIPageViewController *nativePageViewController;
@@ -39,10 +40,32 @@ using namespace facebook::react;
39
40
  NSInteger _pendingGoToIndex;
40
41
  // _nestedScrollEnabled: when YES, this inner PagerView coordinates gestures with an outer PagerView
41
42
  BOOL _nestedScrollEnabled;
43
+ NSHashTable<id<RNCPagerViewNestedOverlayObserver>> *_nestedOverlayObservers;
42
44
  // _blockerGesture: gate gesture that blocks inner scroll at page boundaries when nested
43
45
  UIPanGestureRecognizer *_blockerGesture;
44
46
  }
45
47
 
48
+ - (void)notifyNestedOverlayObservers {
49
+ UIView *activePage = _currentIndex >= 0 &&
50
+ _currentIndex < _nativeChildrenViewControllers.count
51
+ ? _nativeChildrenViewControllers[_currentIndex].view : nil;
52
+ for (id<RNCPagerViewNestedOverlayObserver> observer in _nestedOverlayObservers) {
53
+ BOOL active = activePage != nil &&
54
+ [(UIView *)observer isDescendantOfView:activePage];
55
+ [observer outerPagerDidUpdateOverlayOwner:active transitioning:transitioning || scrollView.dragging || scrollView.decelerating];
56
+ }
57
+ }
58
+
59
+ - (void)addNestedOverlayObserver:(id<RNCPagerViewNestedOverlayObserver>)observer {
60
+ if (_nestedOverlayObservers == nil) _nestedOverlayObservers = [NSHashTable weakObjectsHashTable];
61
+ [_nestedOverlayObservers addObject:observer];
62
+ [self notifyNestedOverlayObservers];
63
+ }
64
+
65
+ - (void)removeNestedOverlayObserver:(id<RNCPagerViewNestedOverlayObserver>)observer {
66
+ [_nestedOverlayObservers removeObject:observer];
67
+ }
68
+
46
69
  // Needed because of this: https://github.com/facebook/react-native/pull/37274
47
70
  + (void)load
48
71
  {
@@ -158,6 +181,7 @@ using namespace facebook::react;
158
181
  -(void)prepareForRecycle {
159
182
  _isBeingRecycled = YES;
160
183
  _generation++;
184
+ [_nestedOverlayObservers removeAllObjects];
161
185
 
162
186
  // Cancel any ongoing transitions
163
187
  if (_nativePageViewController != nil && transitioning) {
@@ -339,6 +363,7 @@ using namespace facebook::react;
339
363
  _pendingGoToIndex = -1;
340
364
 
341
365
  transitioning = YES;
366
+ [self notifyNestedOverlayObservers];
342
367
 
343
368
  __weak RNCPagerViewComponentView *weakSelf = self;
344
369
  NSUInteger capturedGeneration = _generation;
@@ -370,6 +395,7 @@ using namespace facebook::react;
370
395
 
371
396
  [strongSelf enableSwipe];
372
397
  strongSelf->_currentIndex = index;
398
+ [strongSelf notifyNestedOverlayObservers];
373
399
  if (strongSelf->_eventEmitter != nullptr ) {
374
400
  const auto eventEmitter = [strongSelf pagerEventEmitter];
375
401
  if (eventEmitter) {
@@ -422,6 +448,7 @@ using namespace facebook::react;
422
448
  if (_isBeingRecycled) {
423
449
  return;
424
450
  }
451
+ [self notifyNestedOverlayObservers];
425
452
  const auto eventEmitter = [self pagerEventEmitter];
426
453
  if (!eventEmitter) {
427
454
  return;
@@ -455,7 +482,12 @@ using namespace facebook::react;
455
482
  }
456
483
  }
457
484
 
485
+ - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
486
+ if (!decelerate) [self notifyNestedOverlayObservers];
487
+ }
488
+
458
489
  - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
490
+ [self notifyNestedOverlayObservers];
459
491
  if (_isBeingRecycled) {
460
492
  return;
461
493
  }
@@ -529,12 +561,14 @@ using namespace facebook::react;
529
561
  UIViewController* currentVC = [self currentlyDisplayed];
530
562
  NSUInteger currentIndex = [_nativeChildrenViewControllers indexOfObject:currentVC];
531
563
  _currentIndex = currentIndex;
564
+ [self notifyNestedOverlayObservers];
532
565
  int position = (int) currentIndex;
533
566
  const auto eventEmitter = [self pagerEventEmitter];
534
567
  if (eventEmitter) {
535
568
  eventEmitter->onPageSelected(RNCViewPagerEventEmitter::OnPageSelected{.position = static_cast<double>(position)});
536
569
  }
537
570
  }
571
+ if (!completed) [self notifyNestedOverlayObservers];
538
572
  }
539
573
 
540
574
  #pragma mark - UIPageViewControllerDataSource
@@ -23,6 +23,7 @@ const pageKey = (child, index) => {
23
23
  */
24
24
  export class CollapsiblePagerView extends React.PureComponent {
25
25
  state = {
26
+ topOverlayHeight: 0,
26
27
  selectedPage: clampedPageIndex(this.props.initialPage ?? 0, React.Children.toArray(this.props.children).length),
27
28
  transientRetainedPages: []
28
29
  };
@@ -195,6 +196,10 @@ export class CollapsiblePagerView extends React.PureComponent {
195
196
  stickyHeader,
196
197
  headerHeight,
197
198
  stickyHeaderHeight,
199
+ topOverlay,
200
+ externalTopOverlayTag,
201
+ topScrollEdgeEffect,
202
+ topOverlayColorScheme,
198
203
  nativeSmoothHeaderScrollEnabled,
199
204
  pageRetentionDistance: _pageRetentionDistance,
200
205
  onMountedPagesChanged: _onMountedPagesChanged,
@@ -211,6 +216,9 @@ export class CollapsiblePagerView extends React.PureComponent {
211
216
  ...nativeProps
212
217
  } = this.props;
213
218
  const pages = this.pages();
219
+ const supportsTopScrollEdge = Platform.OS === "ios" && Number(Platform.Version) >= 26;
220
+ const hardScrollEdge = supportsTopScrollEdge && topScrollEdgeEffect === "hard";
221
+ const hasTopOverlay = hardScrollEdge && topOverlay != null;
214
222
  const pageKeys = pages.map(pageKey);
215
223
  const retained = new Set(this.mountedPages(pages.length));
216
224
  const deducedLayoutDirection = !layoutDirection || layoutDirection === "locale" ? I18nManager.isRTL ? "rtl" : "ltr" : layoutDirection;
@@ -235,6 +243,10 @@ export class CollapsiblePagerView extends React.PureComponent {
235
243
  offscreenPageLimit: Math.max(1, offscreenPageLimit ?? 1),
236
244
  headerHeight: Math.max(0, Math.round(headerHeight)),
237
245
  stickyHeaderHeight: Math.max(0, Math.round(stickyHeaderHeight)),
246
+ topScrollEdgeHard: hardScrollEdge,
247
+ topOverlayColorScheme: hardScrollEdge ? topOverlayColorScheme : undefined,
248
+ topOverlayHeight: hasTopOverlay ? this.state.topOverlayHeight : 0,
249
+ externalTopOverlayTag: supportsTopScrollEdge && !hasTopOverlay ? externalTopOverlayTag ?? 0 : 0,
238
250
  nativeSmoothHeaderScrollEnabled: nativeSmoothHeaderScrollEnabled,
239
251
  pageKeys: JSON.stringify(pageKeys),
240
252
  retainedPages: JSON.stringify([...retained]),
@@ -273,7 +285,21 @@ export class CollapsiblePagerView extends React.PureComponent {
273
285
  height: Math.max(0, Math.round(stickyHeaderHeight))
274
286
  }],
275
287
  children: stickyHeader
276
- }, "sticky-header"), pages.map((page, index) => /*#__PURE__*/_jsx(View, {
288
+ }, "sticky-header"), hasTopOverlay ? /*#__PURE__*/_jsx(View, {
289
+ collapsable: false,
290
+ style: [styles.slot, {
291
+ bottom: undefined
292
+ }],
293
+ onLayout: event => {
294
+ const height = Math.ceil(event.nativeEvent.layout.height);
295
+ if (this.state.topOverlayHeight !== height) {
296
+ this.setState({
297
+ topOverlayHeight: height
298
+ });
299
+ }
300
+ },
301
+ children: topOverlay
302
+ }, "top-overlay") : null, pages.map((page, index) => /*#__PURE__*/_jsx(View, {
277
303
  collapsable: false,
278
304
  style: [styles.slot,
279
305
  // Android translates the expanded pager while consuming header scroll.
@@ -483,6 +483,10 @@ export class CollapsiblePagerView extends React.PureComponent {
483
483
  children: _children,
484
484
  header,
485
485
  stickyHeader,
486
+ topOverlay: _topOverlay,
487
+ topScrollEdgeEffect: _topScrollEdgeEffect,
488
+ topOverlayColorScheme: _topOverlayColorScheme,
489
+ externalTopOverlayTag: _externalTopOverlayTag,
486
490
  headerHeight: _headerHeight,
487
491
  stickyHeaderHeight: _stickyHeaderHeight,
488
492
  pageRetentionDistance: _pageRetentionDistance,
@@ -43,6 +43,7 @@ export type OnNativeTabPressEventData = Readonly<{
43
43
  position: Int32;
44
44
  key: string;
45
45
  }>;
46
+ export type OnScrollEdgeCapabilityEventData = Readonly<{ available: boolean }>;
46
47
 
47
48
  /**
48
49
  * Internal native props. Consumers should use CollapsiblePagerViewProps from
@@ -58,6 +59,10 @@ export interface NativeProps extends ViewProps {
58
59
  nativeSmoothHeaderScrollEnabled?: WithDefault<boolean, false>;
59
60
  headerHeight?: Int32;
60
61
  stickyHeaderHeight?: Int32;
62
+ topScrollEdgeHard?: WithDefault<boolean, false>;
63
+ topOverlayColorScheme?: string;
64
+ topOverlayHeight?: WithDefault<Double, 0>;
65
+ externalTopOverlayTag?: WithDefault<Int32, 0>;
61
66
  pageKeys?: string;
62
67
  retainedPages?: string;
63
68
  nativeTabBarItems?: string;
@@ -80,6 +85,7 @@ export interface NativeProps extends ViewProps {
80
85
  onCollapsibleStateChanged?: DirectEventHandler<OnCollapsibleStateChangedEventData>;
81
86
  onNativeTabPress?: DirectEventHandler<OnNativeTabPressEventData>;
82
87
  onNativeSubHeaderPress?: DirectEventHandler<OnNativeTabPressEventData>;
88
+ onScrollEdgeCapability?: DirectEventHandler<OnScrollEdgeCapabilityEventData>;
83
89
  }
84
90
 
85
91
  type CollapsiblePagerViewNativeType = HostComponent<NativeProps>;
@@ -3,7 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { findNodeHandle, Keyboard, Platform, StyleSheet, TextInput, View } from "react-native";
5
5
  import NativeScrollerHost, { Commands } from "./NativeScrollerNativeComponent";
6
- import { jsx as _jsx } from "react/jsx-runtime";
6
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  export const NATIVE_SCROLLER_NATIVE_ID_PREFIX = "rnc-collapsible-pager-native-scroller";
8
8
 
9
9
  /** Supported vertical subset; horizontal, snapping, zoom and sticky rows are not supported. */
@@ -27,6 +27,8 @@ const styles = StyleSheet.create({
27
27
  });
28
28
  export const NativeScroller = /*#__PURE__*/React.forwardRef(function NativeScroller({
29
29
  children,
30
+ topOverlay,
31
+ topScrollEdgeEffect,
30
32
  nativeID,
31
33
  pagerScrollKey,
32
34
  style,
@@ -56,6 +58,8 @@ export const NativeScroller = /*#__PURE__*/React.forwardRef(function NativeScrol
56
58
  width: 0,
57
59
  height: 0
58
60
  });
61
+ const [topOverlayHeight, setTopOverlayHeight] = React.useState(0);
62
+ const hasTopOverlay = Platform.OS === "ios" && Number(Platform.Version) >= 26 && topScrollEdgeEffect === "hard" && topOverlay != null;
59
63
  const [refreshEpoch, requestRefreshSync] = React.useReducer(value => value + 1, 0);
60
64
  const contentSizeRef = React.useRef(contentSize);
61
65
  const scrolledSinceGrant = React.useRef(false);
@@ -94,92 +98,111 @@ export const NativeScroller = /*#__PURE__*/React.forwardRef(function NativeScrol
94
98
  blur: () => hostRef.current?.blur()
95
99
  }), []);
96
100
  const keyboardIsDismissible = () => Boolean(TextInput.State.currentlyFocusedInput() && (Keyboard.isVisible() || Platform.OS === "android" && Number(Platform.Version) < 30) && Keyboard.metrics()?.height !== 0);
97
- return /*#__PURE__*/_jsx(NativeScrollerHost, {
98
- ...props,
99
- ref: hostRef,
100
- nativeID: `${NATIVE_SCROLLER_NATIVE_ID_PREFIX}:${pagerScrollKey ?? nativeID ?? generatedId}`,
101
+ return /*#__PURE__*/_jsxs(View, {
102
+ collapsable: false,
101
103
  style: [styles.viewport, style],
102
- nestedScrollEnabled: nestedScrollEnabled,
103
- contentWidth: contentSize.width,
104
- contentHeight: contentSize.height,
105
- contentInsetTop: contentInset?.top ?? 0,
106
- contentInsetRight: contentInset?.right ?? 0,
107
- contentInsetBottom: contentInset?.bottom ?? 0,
108
- contentInsetLeft: contentInset?.left ?? 0,
109
- decelerationRate: rate,
110
- keyboardShouldPersistTaps: persistTaps,
111
- refreshEnabled: Boolean(refresh) && refresh?.enabled !== false,
112
- refreshing: refresh?.refreshing ?? false,
113
- refreshTintColor: refresh?.tintColor,
114
- refreshColors: refresh?.colors,
115
- refreshProgressBackgroundColor: refresh?.progressBackgroundColor,
116
- refreshProgressViewOffset: refresh?.progressViewOffset ?? 0,
117
- refreshTitle: refresh?.title,
118
- refreshTitleColor: refresh?.titleColor,
119
- refreshSize: refresh?.size === "large" ? "large" : "default",
120
- onRefresh: () => {
121
- try {
122
- refresh?.onRefresh?.();
123
- } finally {
124
- requestRefreshSync();
125
- }
126
- },
127
104
  onLayout: onLayout,
128
- onContentViewportChange: event => {
129
- const height = event.nativeEvent.height;
130
- if (!Number.isFinite(height) || height < 0) return;
131
- setContentViewportHeight(previous => previous === height ? previous : height);
132
- },
133
- onScroll: event => {
134
- scrolledSinceGrant.current = true;
135
- onScroll?.(event);
136
- },
137
- onScrollBeginDrag: event => {
138
- scrolledSinceGrant.current = true;
139
- onScrollBeginDrag?.(event);
140
- },
141
- onMomentumScrollBegin: event => {
142
- momentum.current = true;
143
- onMomentumScrollBegin?.(event);
144
- },
145
- onMomentumScrollEnd: event => {
146
- momentum.current = false;
147
- onMomentumScrollEnd?.(event);
148
- },
149
- onStartShouldSetResponder: event => onStartShouldSetResponder?.(event) ?? (persistTaps === "handled" && keyboardIsDismissible() && event.target !== TextInput.State.currentlyFocusedInput()),
150
- onStartShouldSetResponderCapture: event => onStartShouldSetResponderCapture?.(event) ?? (momentum.current || persistTaps === "never" && keyboardIsDismissible() && typeof event.target !== "number" && !textInputState.isTextInput(event.target)),
151
- onResponderGrant: event => {
152
- scrolledSinceGrant.current = false;
153
- grantedDuringMomentum.current = momentum.current;
154
- onResponderGrant?.(event);
155
- },
156
- onResponderRelease: event => {
157
- onResponderRelease?.(event);
158
- if (persistTaps !== "always" && keyboardIsDismissible() && event.target !== TextInput.State.currentlyFocusedInput() && !scrolledSinceGrant.current && !grantedDuringMomentum.current) Keyboard.dismiss();
159
- },
160
- onResponderTerminationRequest: event => onResponderTerminationRequest?.(event) ?? true,
161
- children: /*#__PURE__*/_jsx(View, {
105
+ children: [/*#__PURE__*/_jsx(NativeScrollerHost, {
106
+ ...props,
107
+ ref: hostRef,
108
+ nativeID: `${NATIVE_SCROLLER_NATIVE_ID_PREFIX}:${pagerScrollKey ?? nativeID ?? generatedId}`,
109
+ style: StyleSheet.absoluteFill,
110
+ nestedScrollEnabled: nestedScrollEnabled,
111
+ contentWidth: contentSize.width,
112
+ contentHeight: contentSize.height,
113
+ contentInsetTop: contentInset?.top ?? 0,
114
+ contentInsetRight: contentInset?.right ?? 0,
115
+ contentInsetBottom: contentInset?.bottom ?? 0,
116
+ contentInsetLeft: contentInset?.left ?? 0,
117
+ topScrollEdgeHard: hasTopOverlay,
118
+ topOverlayHeight: hasTopOverlay ? topOverlayHeight : 0,
119
+ decelerationRate: rate,
120
+ keyboardShouldPersistTaps: persistTaps,
121
+ refreshEnabled: Boolean(refresh) && refresh?.enabled !== false,
122
+ refreshing: refresh?.refreshing ?? false,
123
+ refreshTintColor: refresh?.tintColor,
124
+ refreshColors: refresh?.colors,
125
+ refreshProgressBackgroundColor: refresh?.progressBackgroundColor,
126
+ refreshProgressViewOffset: refresh?.progressViewOffset ?? 0,
127
+ refreshTitle: refresh?.title,
128
+ refreshTitleColor: refresh?.titleColor,
129
+ refreshSize: refresh?.size === "large" ? "large" : "default",
130
+ onRefresh: () => {
131
+ try {
132
+ refresh?.onRefresh?.();
133
+ } finally {
134
+ requestRefreshSync();
135
+ }
136
+ },
137
+ onContentViewportChange: event => {
138
+ const height = event.nativeEvent.height;
139
+ if (!Number.isFinite(height) || height < 0) return;
140
+ setContentViewportHeight(previous => previous === height ? previous : height);
141
+ },
142
+ onScroll: event => {
143
+ scrolledSinceGrant.current = true;
144
+ onScroll?.(event);
145
+ },
146
+ onScrollBeginDrag: event => {
147
+ scrolledSinceGrant.current = true;
148
+ onScrollBeginDrag?.(event);
149
+ },
150
+ onMomentumScrollBegin: event => {
151
+ momentum.current = true;
152
+ onMomentumScrollBegin?.(event);
153
+ },
154
+ onMomentumScrollEnd: event => {
155
+ momentum.current = false;
156
+ onMomentumScrollEnd?.(event);
157
+ },
158
+ onStartShouldSetResponder: event => onStartShouldSetResponder?.(event) ?? (persistTaps === "handled" && keyboardIsDismissible() && event.target !== TextInput.State.currentlyFocusedInput()),
159
+ onStartShouldSetResponderCapture: event => onStartShouldSetResponderCapture?.(event) ?? (momentum.current || persistTaps === "never" && keyboardIsDismissible() && typeof event.target !== "number" && !textInputState.isTextInput(event.target)),
160
+ onResponderGrant: event => {
161
+ scrolledSinceGrant.current = false;
162
+ grantedDuringMomentum.current = momentum.current;
163
+ onResponderGrant?.(event);
164
+ },
165
+ onResponderRelease: event => {
166
+ onResponderRelease?.(event);
167
+ if (persistTaps !== "always" && keyboardIsDismissible() && event.target !== TextInput.State.currentlyFocusedInput() && !scrolledSinceGrant.current && !grantedDuringMomentum.current) Keyboard.dismiss();
168
+ },
169
+ onResponderTerminationRequest: event => onResponderTerminationRequest?.(event) ?? true,
170
+ children: /*#__PURE__*/_jsx(View, {
171
+ collapsable: false,
172
+ style: [contentContainerStyle, (contentStyle?.flexGrow ?? 0) > 0 && (contentStyle?.minHeight === undefined || typeof contentStyle.minHeight === "number") ? {
173
+ minHeight: Math.max(contentViewportHeight, contentStyle?.minHeight ?? 0)
174
+ } : undefined, styles.content],
175
+ onLayout: event => {
176
+ const {
177
+ width,
178
+ height
179
+ } = event.nativeEvent.layout;
180
+ const previous = contentSizeRef.current;
181
+ if (previous.width === width && previous.height === height) return;
182
+ const next = {
183
+ width,
184
+ height
185
+ };
186
+ contentSizeRef.current = next;
187
+ setContentSize(next);
188
+ onContentSizeChange?.(width, height);
189
+ },
190
+ children: children
191
+ })
192
+ }), hasTopOverlay ? /*#__PURE__*/_jsx(View, {
162
193
  collapsable: false,
163
- style: [contentContainerStyle, (contentStyle?.flexGrow ?? 0) > 0 && (contentStyle?.minHeight === undefined || typeof contentStyle.minHeight === "number") ? {
164
- minHeight: Math.max(contentViewportHeight, contentStyle?.minHeight ?? 0)
165
- } : undefined, styles.content],
194
+ style: {
195
+ position: "absolute",
196
+ top: 0,
197
+ left: 0,
198
+ right: 0
199
+ },
166
200
  onLayout: event => {
167
- const {
168
- width,
169
- height
170
- } = event.nativeEvent.layout;
171
- const previous = contentSizeRef.current;
172
- if (previous.width === width && previous.height === height) return;
173
- const next = {
174
- width,
175
- height
176
- };
177
- contentSizeRef.current = next;
178
- setContentSize(next);
179
- onContentSizeChange?.(width, height);
201
+ const height = Math.ceil(event.nativeEvent.layout.height);
202
+ setTopOverlayHeight(previous => previous === height ? previous : height);
180
203
  },
181
- children: children
182
- })
204
+ children: topOverlay
205
+ }) : null]
183
206
  });
184
207
  });
185
208
  NativeScroller.displayName = "NativeScroller";