@onekeyfe/react-native-pager-view 3.0.162-alpha.270 → 3.0.162

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.
package/docs/SPEC.md CHANGED
@@ -165,6 +165,28 @@ warnings were observed. Keyboard, multi-touch, physical-device performance and
165
165
  the full lifecycle/interaction matrix remain unverified. These are local source
166
166
  builds; the currently installed npm version does not yet contain this migration.
167
167
 
168
+ ## iOS vertical-scroll press cancellation
169
+
170
+ Status: implemented in source. A source-identical `node_modules` build was
171
+ runtime verified on a dedicated iOS 26.5 HomePager simulator on 2026-09-30:
172
+ a slow drag from the Show more button did not activate it, a fling from the
173
+ same button scrolled the list, and a stationary tap activated it. The Pager
174
+ XCTest suite passed 47/47 tests on a separate iOS 26.5 simulator with its data
175
+ on the external drive on 2026-09-30.
176
+
177
+ In `CollapsiblePagerView` smooth-header mode, a single-finger
178
+ vertical drag beginning on React page content MUST cancel that Surface's active
179
+ React press before finger-up once vertical displacement reaches 10 points and
180
+ exceeds horizontal displacement. Cancellation MUST leave the native vertical
181
+ scroller's pan and momentum intact. Stationary taps, movement below the threshold,
182
+ horizontal drags, and multi-touch sequences MUST retain their existing behavior.
183
+ The iOS content press bridge owns cancellation on the UI thread; the pager's
184
+ existing horizontal-drag cancellation remains independent. The bridge MUST clear
185
+ per-touch state on end, cancellation, and reuse. Android and Web behavior is
186
+ unchanged. Acceptance requires a real iOS button-origin drag that does not fire
187
+ the button, a button-origin fling that scrolls, an ordinary tap that fires, and
188
+ focused native touch-delivery coverage.
189
+
168
190
  ## Native tab-press animation default
169
191
 
170
192
  Behavior change: previously an omitted `nativeTabPressAnimationEnabled` animated
@@ -306,6 +328,24 @@ and refresh completion, caller offset changes, late installation/replacement,
306
328
  retained-page switching, clipping and nested-owner cleanup. Programmatic UIKit
307
329
  unit tests do not substitute for real pull-gesture visibility acceptance.
308
330
 
331
+ When a smooth shared header is attached to the active iOS scroll view, UIKit's
332
+ temporary refresh top inset MUST keep the header and content aligned. Both are
333
+ children of that scroll view and move together with its content offset; the
334
+ pager MUST NOT apply a separate refresh translation to the header. The scroll
335
+ view's inset and offset, the refresh indicator, and refresh callback ownership
336
+ remain unchanged. Removing the refresh inset or control preserves this alignment
337
+ even without a scroll-offset event. Caller inset changes update the pager
338
+ baseline. The sticky tab row MUST pin against the baseline top inset; the
339
+ refresh control's temporary inset MUST NOT move it below the content, including
340
+ rapid direction reversals while the spinner is active. Detaching the header
341
+ restores its normal transform. This applies to RN NativeScroller and NativeList
342
+ pages; non-smooth and detached headers keep
343
+ their existing positioning. The 2026-09-30 Home Spot simulator checkpoint covers
344
+ shared header/content alignment and inset removal. A separate source-identical
345
+ local package build verified the sticky row during active-refresh vertical
346
+ reversals; published-package, other Home pages and physical-device acceptance
347
+ remain open.
348
+
309
349
  ## iOS shared-header hit testing
310
350
 
311
351
  Smooth-header hit forwarding is restricted to the pager's own interactive
@@ -1033,6 +1033,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1033
1033
  __weak UITouch *_trackedTouch;
1034
1034
  UIEvent *_touchEvent;
1035
1035
  BOOL _multipleSurfaceTouchesObserved;
1036
+ CGPoint _trackedTouchStartPoint;
1037
+ BOOL _didCancelReactPressForVerticalMovement;
1036
1038
  }
1037
1039
 
1038
1040
  - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)other
@@ -1069,12 +1071,37 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1069
1071
  return;
1070
1072
  }
1071
1073
  _trackedTouch = touches.anyObject;
1074
+ _trackedTouchStartPoint = [_trackedTouch locationInView:self.view];
1075
+ _didCancelReactPressForVerticalMovement = NO;
1072
1076
  [self recordEvent:event];
1073
1077
  }
1074
1078
 
1075
1079
  - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
1076
1080
  {
1077
1081
  [self recordEvent:event];
1082
+ UITouch *touch = _trackedTouch;
1083
+ UIGestureRecognizer *handler = self.reactTouchHandler;
1084
+ if (touch == nil || ![touches containsObject:touch] ||
1085
+ self.state != UIGestureRecognizerStatePossible ||
1086
+ _didCancelReactPressForVerticalMovement || _multipleSurfaceTouchesObserved ||
1087
+ handler.view == nil || self.view != handler.view.superview ||
1088
+ handler.state == UIGestureRecognizerStateFailed ||
1089
+ handler.state == UIGestureRecognizerStateCancelled ||
1090
+ handler.state == UIGestureRecognizerStateEnded) return;
1091
+ CGPoint point = [touch locationInView:self.view];
1092
+ CGFloat dx = point.x - _trackedTouchStartPoint.x;
1093
+ CGFloat dy = point.y - _trackedTouchStartPoint.y;
1094
+ if (fabs(dy) < 10.0 || fabs(dy) <= fabs(dx)) return;
1095
+ NSUInteger activeTouches = 0;
1096
+ for (UITouch *activeTouch in event.allTouches) {
1097
+ if (activeTouch.phase == UITouchPhaseEnded ||
1098
+ activeTouch.phase == UITouchPhaseCancelled) continue;
1099
+ if ([activeTouch.view isDescendantOfView:handler.view]) ++activeTouches;
1100
+ }
1101
+ if (activeTouches != 1) return;
1102
+ _didCancelReactPressForVerticalMovement = YES;
1103
+ handler.enabled = NO;
1104
+ handler.enabled = YES;
1078
1105
  }
1079
1106
 
1080
1107
  - (void)cancelPressForPan:(UIPanGestureRecognizer *)pan
@@ -1108,6 +1135,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1108
1135
  ? UIGestureRecognizerStateEnded : UIGestureRecognizerStateFailed;
1109
1136
  _trackedTouch = nil;
1110
1137
  _touchEvent = nil;
1138
+ _didCancelReactPressForVerticalMovement = NO;
1111
1139
  }
1112
1140
 
1113
1141
  - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
@@ -1117,6 +1145,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1117
1145
  ? UIGestureRecognizerStateCancelled : UIGestureRecognizerStateFailed;
1118
1146
  _trackedTouch = nil;
1119
1147
  _touchEvent = nil;
1148
+ _didCancelReactPressForVerticalMovement = NO;
1120
1149
  }
1121
1150
 
1122
1151
  - (void)reset
@@ -1125,6 +1154,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1125
1154
  _trackedTouch = nil;
1126
1155
  _touchEvent = nil;
1127
1156
  _multipleSurfaceTouchesObserved = NO;
1157
+ _didCancelReactPressForVerticalMovement = NO;
1128
1158
  }
1129
1159
 
1130
1160
  @end
@@ -2437,6 +2467,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2437
2467
  forKeyPath:@"contentOffset"
2438
2468
  options:NSKeyValueObservingOptionNew
2439
2469
  context:RNCCollapsiblePagerContentOffsetContext];
2470
+ [candidate addObserver:self
2471
+ forKeyPath:@"contentInset"
2472
+ options:NSKeyValueObservingOptionNew
2473
+ context:RNCCollapsiblePagerContentOffsetContext];
2440
2474
  [candidate addObserver:self
2441
2475
  forKeyPath:@"contentSize"
2442
2476
  options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
@@ -2459,6 +2493,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2459
2493
  [scrollView removeObserver:self
2460
2494
  forKeyPath:@"contentOffset"
2461
2495
  context:RNCCollapsiblePagerContentOffsetContext];
2496
+ [scrollView removeObserver:self
2497
+ forKeyPath:@"contentInset"
2498
+ context:RNCCollapsiblePagerContentOffsetContext];
2462
2499
  [scrollView removeObserver:self
2463
2500
  forKeyPath:@"contentSize"
2464
2501
  context:RNCCollapsiblePagerContentOffsetContext];
@@ -2932,6 +2969,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2932
2969
  if (appliedTop != nil) {
2933
2970
  [_appliedTopInsets setObject:@(appliedTop.doubleValue + next.top - previous.top) forKey:scrollView];
2934
2971
  }
2972
+ if (scrollView == _observedScrollView) [self updateHeaderForScrollView:scrollView];
2935
2973
  }
2936
2974
 
2937
2975
  - (void)reapplyInsetsToObservedScrollView
@@ -2964,10 +3002,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2964
3002
  context:(void *)context
2965
3003
  {
2966
3004
  if (context == RNCScrollRefreshControlContext) {
2967
- [self updateRefreshControlForScrollView:(UIScrollView *)object];
3005
+ UIScrollView *scrollView = (UIScrollView *)object;
3006
+ [self updateRefreshControlForScrollView:scrollView];
3007
+ if (scrollView == _observedScrollView) [self updateHeaderForScrollView:scrollView];
2968
3008
  return;
2969
3009
  }
2970
3010
  if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
3011
+ if ([keyPath isEqualToString:@"contentInset"]) {
3012
+ [self updateHeaderForScrollView:(UIScrollView *)object];
3013
+ return;
3014
+ }
2971
3015
  if ([keyPath isEqualToString:@"contentSize"]) {
2972
3016
  NSValue *oldSize = change[NSKeyValueChangeOldKey];
2973
3017
  NSValue *newSize = change[NSKeyValueChangeNewKey];
@@ -2998,9 +3042,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2998
3042
  BOOL attachedToList = _nativeSmoothHeaderScrollEnabled &&
2999
3043
  _sharedHeaderScrollView != nil && !_sharedHeadersLiftedForPagerTransition;
3000
3044
  if (attachedToList) {
3045
+ // The header and content share this scroll view. UIKit's refresh inset
3046
+ // moves both together, so translating the header would separate them.
3001
3047
  _sharedHeaderHostView.transform = CGAffineTransformIdentity;
3002
3048
  _headerView.transform = CGAffineTransformIdentity;
3003
- CGFloat pinnedTranslation = MAX(0, _currentLogicalOffset - _headerHeight);
3049
+ UIScrollView *scrollView = _sharedHeaderScrollView;
3050
+ NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
3051
+ CGFloat refreshInset = scrollView.refreshControl != nil && appliedTop != nil
3052
+ ? MAX(0, scrollView.contentInset.top - appliedTop.doubleValue) : 0;
3053
+ CGFloat pinnedTranslation = MAX(0, _currentLogicalOffset - _headerHeight - refreshInset);
3004
3054
  CGAffineTransform stickyTransform = CGAffineTransformMakeTranslation(0, pinnedTranslation);
3005
3055
  _stickyHeaderView.transform = stickyTransform;
3006
3056
  _nativeTabBarView.transform = stickyTransform;
@@ -744,6 +744,59 @@ typedef struct __IOHIDEvent *RNCTestHIDEventRef;
744
744
  XCTAssertEqual(self.handler.registeredTouches, 0);
745
745
  }
746
746
 
747
+ - (void)testVerticalPageDragCancelsReactPressBeforeFingerLifts
748
+ {
749
+ [self assertFixtureIsReadyForDelivery];
750
+ CGPoint point = [self pressPoint];
751
+ UITouch *touch = [self.driver beginAt:point];
752
+ XCTAssertEqual(self.handler.registeredTouches, 1);
753
+
754
+ [self.driver move:touch to:CGPointMake(point.x, point.y - 12)];
755
+ XCTAssertEqual(self.handler.registeredTouches, 0);
756
+ XCTAssertTrue([self.handler.events containsObject:@"reset-cancelled"] ||
757
+ [self.handler.events containsObject:@"cancelled"],
758
+ @"handler events %@", self.handler.events);
759
+ XCTAssertFalse([self.handler.events containsObject:@"ended"]);
760
+
761
+ [self.driver end:touch];
762
+ XCTAssertFalse([self.handler.events containsObject:@"ended"], @"events %@", self.handler.events);
763
+ }
764
+
765
+ - (void)testVerticalSiblingControlDragKeepsReactPress
766
+ {
767
+ [self assertFixtureIsReadyForDelivery];
768
+ UIView *sibling = [RCTViewComponentView new];
769
+ sibling.frame = CGRectMake(160, 540, 80, 100);
770
+ [self.surface addSubview:sibling];
771
+ CGPoint point = [self pressPoint];
772
+ UIScrollView *pagerScrollView = [self.pager valueForKey:@"pagerScrollView"];
773
+ XCTAssertEqual(sibling.superview, self.pager.superview);
774
+ XCTAssertFalse([sibling isDescendantOfView:pagerScrollView]);
775
+ XCTAssertEqual([self.window hitTest:point withEvent:nil], sibling);
776
+
777
+ UITouch *touch = [self.driver beginAt:point];
778
+ XCTAssertEqual(self.handler.registeredTouches, 1);
779
+ [self.driver move:touch to:CGPointMake(point.x, point.y - 12)];
780
+ XCTAssertEqual(self.handler.registeredTouches, 1);
781
+ XCTAssertFalse([self.handler.events containsObject:@"reset-cancelled"]);
782
+ XCTAssertFalse([self.handler.events containsObject:@"cancelled"]);
783
+
784
+ [self.driver end:touch];
785
+ XCTAssertEqualObjects(self.handler.events, (@[@"began", @"moved", @"ended"]));
786
+ XCTAssertEqual(self.handler.registeredTouches, 0);
787
+ }
788
+
789
+ - (void)testSmallVerticalMovementKeepsReactPress
790
+ {
791
+ [self assertFixtureIsReadyForDelivery];
792
+ CGPoint point = [self pressPoint];
793
+ UITouch *touch = [self.driver beginAt:point];
794
+ [self.driver move:touch to:CGPointMake(point.x, point.y - 8)];
795
+ XCTAssertEqual(self.handler.registeredTouches, 1);
796
+ [self.driver end:touch];
797
+ XCTAssertEqualObjects(self.handler.events, (@[@"began", @"moved", @"ended"]));
798
+ }
799
+
747
800
  - (void)testSmallMovementBelowPanThresholdKeepsReactPress
748
801
  {
749
802
  [self assertFixtureIsReadyForDelivery];
@@ -1,9 +1,11 @@
1
1
  #import <XCTest/XCTest.h>
2
+ #import <React/RCTViewComponentView.h>
2
3
  #import "RNCCollapsiblePagerViewComponentView.h"
3
4
 
4
5
  @interface RNCCollapsiblePagerViewComponentView (RefreshControlTesting)
5
6
  - (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
6
7
  - (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView;
8
+ - (void)attachScrollObserverForCurrentPage;
7
9
  @end
8
10
 
9
11
  @interface RNCCollapsiblePagerRefreshControlTests : XCTestCase
@@ -177,4 +179,71 @@
177
179
  XCTAssertTrue(control.layer.masksToBounds);
178
180
  }
179
181
 
182
+ - (void)testRefreshTopInsetDoesNotDisplaceAttachedSharedHeader
183
+ {
184
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:64];
185
+ [pager mountChildComponentView:[RCTViewComponentView new] index:0];
186
+ [pager mountChildComponentView:[RCTViewComponentView new] index:1];
187
+ UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
188
+ scroll.refreshControl = [UIRefreshControl new];
189
+ scroll.contentSize = CGSizeMake(320, 2000);
190
+ scroll.contentInset = UIEdgeInsetsMake(232, 0, 0, 0);
191
+ UIView *content = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 2000)];
192
+ [scroll addSubview:content];
193
+ UIViewController *page = [UIViewController new];
194
+ [page.view addSubview:scroll];
195
+ NSMutableArray<UIViewController *> *pages = [pager valueForKey:@"pageControllers"];
196
+ [pages addObject:page];
197
+ [pager attachScrollObserverForCurrentPage];
198
+ UIView *headerHost = [pager valueForKey:@"sharedHeaderHostView"];
199
+ UIView *stickyHeader = [pager valueForKey:@"stickyHeaderView"];
200
+ UIView *tabBar = [pager valueForKey:@"nativeTabBarView"];
201
+ XCTAssertNotNil(stickyHeader);
202
+ XCTAssertEqual(stickyHeader.superview, headerHost);
203
+ XCTAssertEqual(headerHost.superview, scroll);
204
+ XCTAssertEqualWithAccuracy(scroll.contentInset.top, 340, 0.001);
205
+ XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
206
+
207
+ scroll.contentOffset = CGPointMake(0, 500);
208
+ CGFloat baselineStickyTranslation = stickyHeader.transform.ty;
209
+ CGFloat baselineTabBarTranslation = tabBar.transform.ty;
210
+ XCTAssertEqualWithAccuracy(baselineStickyTranslation, 776, 0.001);
211
+ XCTAssertEqualWithAccuracy(baselineTabBarTranslation, 776, 0.001);
212
+ CGPoint offset = scroll.contentOffset;
213
+ scroll.contentInset = UIEdgeInsetsMake(400, 0, 0, 0);
214
+ XCTAssertTrue(CGPointEqualToPoint(scroll.contentOffset, offset));
215
+ XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
216
+ XCTAssertEqualWithAccuracy(stickyHeader.transform.ty, baselineStickyTranslation, 0.001);
217
+ XCTAssertEqualWithAccuracy(tabBar.transform.ty, baselineTabBarTranslation, 0.001);
218
+ XCTAssertEqualWithAccuracy(CGRectGetMaxY(headerHost.frame), CGRectGetMinY(content.frame), 0.001);
219
+
220
+ // Exercise actual scroll notifications while the temporary refresh inset is
221
+ // present, including direction reversals and the sticky/unpinned boundary.
222
+ // The pre-fix formula adds the extra 60 points on these scroll updates.
223
+ for (NSNumber *nextOffset in @[@620, @450, @-300, @-340, @500]) {
224
+ scroll.contentOffset = CGPointMake(0, nextOffset.doubleValue);
225
+ CGFloat expectedTranslation = MAX(0, nextOffset.doubleValue + 340 - 64);
226
+ XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
227
+ XCTAssertEqualWithAccuracy(stickyHeader.transform.ty, expectedTranslation, 0.001);
228
+ XCTAssertEqualWithAccuracy(tabBar.transform.ty, expectedTranslation, 0.001);
229
+ XCTAssertEqualWithAccuracy(CGRectGetMaxY(headerHost.frame), CGRectGetMinY(content.frame), 0.001);
230
+ }
231
+
232
+ offset = scroll.contentOffset;
233
+ scroll.contentInset = UIEdgeInsetsMake(340, 0, 0, 0);
234
+ XCTAssertTrue(CGPointEqualToPoint(scroll.contentOffset, offset));
235
+ XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
236
+ XCTAssertEqualWithAccuracy(stickyHeader.transform.ty, baselineStickyTranslation, 0.001);
237
+ XCTAssertEqualWithAccuracy(tabBar.transform.ty, baselineTabBarTranslation, 0.001);
238
+
239
+ scroll.contentInset = UIEdgeInsetsMake(400, 0, 0, 0);
240
+ XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
241
+ XCTAssertEqualWithAccuracy(stickyHeader.transform.ty, baselineStickyTranslation, 0.001);
242
+ XCTAssertEqualWithAccuracy(tabBar.transform.ty, baselineTabBarTranslation, 0.001);
243
+ XCTAssertEqualWithAccuracy(CGRectGetMaxY(headerHost.frame), CGRectGetMinY(content.frame), 0.001);
244
+ scroll.refreshControl = nil;
245
+ XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
246
+ [pager prepareForRecycle];
247
+ }
248
+
180
249
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-pager-view",
3
- "version": "3.0.162-alpha.270",
3
+ "version": "3.0.162",
4
4
  "description": "React Native wrapper for Android and iOS ViewPager",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
@@ -69,7 +69,7 @@
69
69
  "typescript": "^5.9.2"
70
70
  },
71
71
  "peerDependencies": {
72
- "@onekeyfe/react-native-native-logger": "3.0.162-alpha.270",
72
+ "@onekeyfe/react-native-native-logger": "3.0.162",
73
73
  "react": "*",
74
74
  "react-native": "*"
75
75
  },