@onekeyfe/react-native-pager-view 3.0.175 → 3.0.176

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.
@@ -123,6 +123,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
123
123
  private val restoredRecyclerKeys = WeakHashMap<RecyclerView, String>()
124
124
  private val originalScrollerPadding = WeakHashMap<ScrollView, ScrollerPadding>()
125
125
  private val restoredScrollerKeys = WeakHashMap<ScrollView, String>()
126
+ private val deferredScrollerGeometry = WeakHashMap<ScrollView, Triple<Int, Int, Int>>()
126
127
  private val ownedScrollerKeys = WeakHashMap<NativeScrollerViewport, String>()
127
128
  private val ownedScrollerRestoreRequests = HashMap<String, Pair<WeakReference<NativeScrollerViewport>, Int?>>()
128
129
  private var foregroundRefreshIndicator: WeakReference<ImageView>? = null
@@ -513,6 +514,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
513
514
  alignedBodyKeys.clear()
514
515
  transitionAlignedViews.clear()
515
516
  deferredRecyclerGeometry.clear()
517
+ deferredScrollerGeometry.clear()
516
518
  displayBodyOverrides.clear()
517
519
  displayRecyclerAnchors.clear()
518
520
  ownedScrollerRestoreRequests.clear()
@@ -907,6 +909,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
907
909
  pendingPageRestores.remove(key)
908
910
  ownedScrollerRestoreRequests.remove(key)
909
911
  if (view is RecyclerView) deferredRecyclerGeometry.remove(view)
912
+ if (view is ScrollView) deferredScrollerGeometry.remove(view)
910
913
  (view as? NativeScrollerViewport)?.host?.cancelPendingScrollRestore()
911
914
  suspendedBodyKeys.remove(key)
912
915
  alignedBodyKeys.remove(key)
@@ -985,6 +988,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
985
988
  displayRecyclerAnchors.remove(key)
986
989
  recyclerReadingAnchors.remove(key)
987
990
  if (view is RecyclerView) deferredRecyclerGeometry.remove(view)
991
+ if (view is ScrollView) deferredScrollerGeometry.remove(view)
988
992
  pageOffsets[key] = 0
989
993
  val request = PagerTopRequest(view, key, selectedPage, attachmentGeneration, target, ++pagerTopToken)
990
994
  pagerTopRequest = request
@@ -1717,6 +1721,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
1717
1721
  original.appliedTopInset = topInset
1718
1722
 
1719
1723
  val key = pageKey(pageIndex)
1724
+ val geometry = Triple(scroller.getChildAt(0)?.height ?: 0, scroller.height,
1725
+ scroller.paddingTop + scroller.paddingBottom)
1726
+ deferredScrollerGeometry[scroller]?.let { deferred ->
1727
+ if (geometry != deferred) {
1728
+ deferredScrollerGeometry.remove(scroller)
1729
+ restoredScrollerKeys.remove(scroller)
1730
+ if (nativeSmoothHeaderScrollEnabled && headerOffsetPx == headerHeightPx) {
1731
+ displayBodyOverrides.remove(key)
1732
+ }
1733
+ }
1734
+ }
1720
1735
  if (restoredScrollerKeys[scroller] != key) {
1721
1736
  restoredScrollerKeys[scroller] = key
1722
1737
  val restoreRequest = registerPageRestore(key, scroller, pageIndex)
@@ -1744,6 +1759,16 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
1744
1759
  return@post
1745
1760
  }
1746
1761
  if (scroller.scrollY != saved) scroller.scrollTo(scroller.scrollX, saved)
1762
+ val unreachableReadingIntent = nativeSmoothHeaderScrollEnabled &&
1763
+ headerOffsetPx == headerHeightPx && displayBodyOverrides.containsKey(key) &&
1764
+ (pageOffsets[key] ?: 0) > scroller.scrollY && !scroller.canScrollVertically(1)
1765
+ if (nativeSmoothHeaderScrollEnabled && (scroller.scrollY != saved || unreachableReadingIntent)) {
1766
+ deferredScrollerGeometry[scroller] = Triple(scroller.getChildAt(0)?.height ?: 0,
1767
+ scroller.height, scroller.paddingTop + scroller.paddingBottom)
1768
+ offsetDiag("scrollview-restore-pending-saved=$saved-current=${scroller.scrollY}", scroller, key, pageIndex, restoreRequest)
1769
+ return@post
1770
+ }
1771
+ deferredScrollerGeometry.remove(scroller)
1747
1772
  if (nativeSmoothHeaderScrollEnabled) clearPageRestoreForView(key, scroller, restoreRequest)
1748
1773
  else pendingPageRestores.remove(key)
1749
1774
  offsetDiag("scrollview-restore-after-saved=$saved", scroller, key, pageIndex, restoreRequest)
@@ -1813,6 +1838,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
1813
1838
 
1814
1839
  private fun restoreNativeScrollerInsets(scroller: ScrollView) {
1815
1840
  val original = originalScrollerPadding.remove(scroller) ?: return
1841
+ deferredScrollerGeometry.remove(scroller)
1816
1842
  updateScrollerPaddingOwnership(scroller, original)
1817
1843
  restoredScrollerKeys.remove(scroller)?.let { clearPageRestoreForView(it, scroller) }
1818
1844
  if (scroller is ReactScrollView) {
@@ -2193,6 +2219,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
2193
2219
  val request = pageRestoreRequests[currentPageKey()]
2194
2220
  offsetDiag("touch-cancel-before", target, request = request)
2195
2221
  observedRecyclerView?.let { deferredRecyclerGeometry.remove(it) }
2222
+ observedNativeScroller?.let { deferredScrollerGeometry.remove(it) }
2196
2223
  pageRestoreRequests.remove(currentPageKey())
2197
2224
  pendingPageRestores.remove(currentPageKey())
2198
2225
  ownedScrollerRestoreRequests.remove(currentPageKey())
@@ -2334,6 +2361,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
2334
2361
  alignedBodyKeys.clear()
2335
2362
  transitionAlignedViews.clear()
2336
2363
  deferredRecyclerGeometry.clear()
2364
+ deferredScrollerGeometry.clear()
2337
2365
  displayBodyOverrides.clear()
2338
2366
  displayRecyclerAnchors.clear()
2339
2367
  ownedScrollerRestoreRequests.clear()
package/docs/SPEC.md CHANGED
@@ -38,6 +38,12 @@ Late callbacks cannot revive cancelled restoration or erase a newer request.
38
38
  Observers and commands must match page key, view identity and generation.
39
39
  Removed or recycled owners cannot apply callbacks to another page; recycling
40
40
  resets owned memory.
41
+ The Web backend shares C and stores per-page B separately from absolute DOM
42
+ scrollTop. Selection aligns to C while expanded and C + B while fully collapsed;
43
+ programmatic alignment and geometry-only scrollend events MUST NOT erase the
44
+ suspended reading position. Real vertical input or a valid List command takes
45
+ over the current display. PageTop/StickyTop clear only the current B.
46
+
41
47
  Legacy mode and pagers without a collapsible header retain their existing
42
48
  behavior. Vertical scrolling, refresh, snapping and repeated-current-tab behavior
43
49
  are unchanged. This state belongs to native UI instances, with no new React prop
@@ -44,6 +44,8 @@ export class CollapsiblePagerView extends React.PureComponent {
44
44
  pageOffsets = new Map();
45
45
  pageHosts = new Map();
46
46
  sharedHeaderOffset = 0;
47
+ selectionRestoreToken = 0;
48
+ bodyAlignment = null;
47
49
  scrollEnabled = this.props.scrollEnabled ?? true;
48
50
  lastMountSignature = null;
49
51
  configureFrame = null;
@@ -71,6 +73,7 @@ export class CollapsiblePagerView extends React.PureComponent {
71
73
  const pageCount = this.pages().length;
72
74
  const selectedPage = clampedPageIndex(this.state.selectedPage, pageCount);
73
75
  if (selectedPage !== this.state.selectedPage) {
76
+ this.selectionRestoreToken += 1;
74
77
  this.cancelPendingTopFrame();
75
78
  this.finishPageTransition();
76
79
  this.setState({
@@ -83,6 +86,7 @@ export class CollapsiblePagerView extends React.PureComponent {
83
86
  this.notifyMountedPagesChanged();
84
87
  }
85
88
  componentWillUnmount() {
89
+ this.selectionRestoreToken += 1;
86
90
  this.cancelPendingTopFrame();
87
91
  this.savePageOffset(this.state.selectedPage);
88
92
  if (this.configureFrame !== null) {
@@ -116,16 +120,32 @@ export class CollapsiblePagerView extends React.PureComponent {
116
120
  if (this.pageScrollElement(this.state.selectedPage) === pending.element && this.pages().map(pageKey)[this.state.selectedPage] === pending.key) this.savePageOffset(this.state.selectedPage);
117
121
  };
118
122
  onQueuedTopWheel = event => {
119
- if (event.deltaY !== 0) this.cancelPendingTopFrame();
123
+ if (event.deltaY !== 0) this.takeOverVerticalScroll();
124
+ };
125
+ onScrollbarPointerDown = event => {
126
+ const element = this.pageScrollElement(this.state.selectedPage);
127
+ if (!element || event.target !== element || event.button !== 0) return;
128
+ const rect = element.getBoundingClientRect();
129
+ const computed = element.ownerDocument.defaultView?.getComputedStyle(element);
130
+ const borderLeft = Number.parseFloat(computed?.borderLeftWidth ?? "0") || 0;
131
+ const borderRight = Number.parseFloat(computed?.borderRightWidth ?? "0") || 0;
132
+ const clientStart = rect.left + element.clientLeft;
133
+ const clientEnd = clientStart + element.clientWidth;
134
+ const leftGutter = clientStart - (rect.left + borderLeft);
135
+ const rightGutter = rect.right - borderRight - clientEnd;
136
+ const inVerticalTrack = event.clientY >= rect.top + element.clientTop && event.clientY < rect.top + element.clientTop + element.clientHeight;
137
+ const inGutter = leftGutter > 0 && event.clientX >= rect.left + borderLeft && event.clientX < clientStart || rightGutter > 0 && event.clientX >= clientEnd && event.clientX < rect.right - borderRight;
138
+ if (inVerticalTrack && inGutter) this.takeOverVerticalScroll();
120
139
  };
121
140
  onQueuedTopKey = event => {
122
- if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) this.cancelPendingTopFrame();
141
+ if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) this.takeOverVerticalScroll();
123
142
  };
124
143
  scrollCurrentPageToTop(offset, animated) {
125
144
  if (this.horizontalTouch || this.transitionTimer !== null) return;
126
145
  const element = this.pageScrollElement(this.state.selectedPage);
127
146
  const key = this.pages().map(pageKey)[this.state.selectedPage];
128
147
  if (!element?.isConnected || !key) return;
148
+ this.selectionRestoreToken += 1;
129
149
  this.cancelPendingTopFrame();
130
150
  const EventConstructor = element.ownerDocument.defaultView?.Event;
131
151
  if (EventConstructor) element.dispatchEvent(new EventConstructor("ok-collapsible-pager-cancel-scroll-command"));
@@ -138,8 +158,12 @@ export class CollapsiblePagerView extends React.PureComponent {
138
158
  if (this.pendingTopFrame?.frame !== frame) return;
139
159
  this.pendingTopFrame = null;
140
160
  if (!element.isConnected || this.horizontalTouch || this.transitionTimer !== null || this.pageScrollElement(this.state.selectedPage) !== element || this.pages().map(pageKey)[this.state.selectedPage] !== key) return;
141
- this.pageOffsets.set(key, offset);
161
+ this.pageOffsets.set(key, 0);
142
162
  this.sharedHeaderOffset = offset;
163
+ this.bodyAlignment = {
164
+ element,
165
+ key
166
+ };
143
167
  element.scrollTo({
144
168
  top: offset,
145
169
  behavior: animated ? "smooth" : "instant"
@@ -218,8 +242,9 @@ export class CollapsiblePagerView extends React.PureComponent {
218
242
  const configured = this.configuredScrollElement;
219
243
  if (!configured) return;
220
244
  this.cancelPendingTopFrame();
221
- configured.element.removeEventListener("ok-native-list-scroll-command-accepted", this.cancelPendingTopFrame);
245
+ configured.element.removeEventListener("ok-native-list-scroll-command-accepted", this.onListScrollCommandAccepted);
222
246
  configured.element.removeEventListener("wheel", this.onQueuedTopWheel);
247
+ configured.element.removeEventListener("pointerdown", this.onScrollbarPointerDown);
223
248
  configured.element.removeEventListener("keydown", this.onQueuedTopKey);
224
249
  configured.element.removeEventListener("scrollend", this.onVerticalScrollSettled);
225
250
  const style = timelineStyle(configured.element);
@@ -234,6 +259,7 @@ export class CollapsiblePagerView extends React.PureComponent {
234
259
  configured.element.removeAttribute("data-collapsible-pager-active");
235
260
  this.notifyInsetsChanged(configured.element);
236
261
  this.configuredScrollElement = null;
262
+ if (this.bodyAlignment?.element === configured.element) this.bodyAlignment = null;
237
263
  }
238
264
  configureActiveScrollElement = () => {
239
265
  this.configureFrame = null;
@@ -269,8 +295,9 @@ export class CollapsiblePagerView extends React.PureComponent {
269
295
  pagerStickyInset: style.getPropertyValue(PAGER_STICKY_INSET)
270
296
  };
271
297
  element.addEventListener("scrollend", this.onVerticalScrollSettled);
272
- element.addEventListener("ok-native-list-scroll-command-accepted", this.cancelPendingTopFrame);
298
+ element.addEventListener("ok-native-list-scroll-command-accepted", this.onListScrollCommandAccepted);
273
299
  element.addEventListener("wheel", this.onQueuedTopWheel);
300
+ element.addEventListener("pointerdown", this.onScrollbarPointerDown);
274
301
  element.addEventListener("keydown", this.onQueuedTopKey);
275
302
  }
276
303
  const style = timelineStyle(element);
@@ -299,8 +326,18 @@ export class CollapsiblePagerView extends React.PureComponent {
299
326
  this.configureActiveScrollElement();
300
327
  }
301
328
  }
302
- onVerticalScrollSettled = () => {
303
- if (this.pendingTopFrame) return;
329
+ takeOverVerticalScroll = () => {
330
+ this.selectionRestoreToken += 1;
331
+ this.bodyAlignment = null;
332
+ this.cancelPendingTopFrame();
333
+ this.savePageOffset(this.state.selectedPage);
334
+ };
335
+ onListScrollCommandAccepted = event => {
336
+ if (event.target !== this.pageScrollElement(this.state.selectedPage)) return;
337
+ this.takeOverVerticalScroll();
338
+ };
339
+ onVerticalScrollSettled = event => {
340
+ if (this.pendingTopFrame || event && event.target !== this.pageScrollElement(this.state.selectedPage)) return;
304
341
  this.savePageOffset(this.state.selectedPage);
305
342
  this.emitDiagnostics("scroll-settled");
306
343
  };
@@ -308,15 +345,24 @@ export class CollapsiblePagerView extends React.PureComponent {
308
345
  const key = this.pages().map(pageKey)[index];
309
346
  const scrollElement = this.pageScrollElement(index);
310
347
  if (key && scrollElement) {
311
- this.pageOffsets.set(key, scrollElement.scrollTop);
312
- this.sharedHeaderOffset = Math.min(Math.max(scrollElement.scrollTop, 0), Math.max(0, this.props.headerHeight));
348
+ if (this.bodyAlignment?.key === key && this.bodyAlignment.element === scrollElement) return;
349
+ const D = Math.max(0, this.props.headerHeight);
350
+ this.pageOffsets.set(key, Math.max(0, scrollElement.scrollTop - D));
351
+ this.sharedHeaderOffset = Math.min(Math.max(scrollElement.scrollTop, 0), D);
313
352
  }
314
353
  }
315
354
  restorePageOffset(index) {
316
355
  const key = this.pages().map(pageKey)[index];
317
356
  const scrollElement = this.pageScrollElement(index);
318
357
  if (!key || !scrollElement) return;
319
- scrollElement.scrollTop = Math.max(this.pageOffsets.get(key) ?? this.sharedHeaderOffset, this.sharedHeaderOffset);
358
+ const D = Math.max(0, this.props.headerHeight);
359
+ const C = Math.min(Math.max(0, this.sharedHeaderOffset), D);
360
+ const expectedQ = C + (C === D ? this.pageOffsets.get(key) ?? 0 : 0);
361
+ this.bodyAlignment = {
362
+ element: scrollElement,
363
+ key
364
+ };
365
+ scrollElement.scrollTop = expectedQ;
320
366
  }
321
367
  selectPage(selectedPage, animated) {
322
368
  const pages = this.pages();
@@ -324,6 +370,8 @@ export class CollapsiblePagerView extends React.PureComponent {
324
370
  if (position === null || position < 0 || position >= pages.length || position === this.state.selectedPage) {
325
371
  return;
326
372
  }
373
+ const selectionToken = ++this.selectionRestoreToken;
374
+ const expectedKey = this.pages().map(pageKey)[position];
327
375
  this.cancelPendingTopFrame();
328
376
  this.savePageOffset(this.state.selectedPage);
329
377
  if (animated) this.beginPageTransition();else this.finishPageTransition();
@@ -333,6 +381,7 @@ export class CollapsiblePagerView extends React.PureComponent {
333
381
  }, () => {
334
382
  this.scheduleScrollConfiguration();
335
383
  const restore = () => {
384
+ if (this.selectionRestoreToken !== selectionToken || this.state.selectedPage !== position || this.pages().map(pageKey)[position] !== expectedKey || !this.pageScrollElement(position)?.isConnected) return;
336
385
  this.configureActiveScrollElement();
337
386
  this.restorePageOffset(position);
338
387
  };
@@ -392,7 +441,7 @@ export class CollapsiblePagerView extends React.PureComponent {
392
441
  if (!touch) return;
393
442
  const deltaX = touch.pageX - this.touchStartX;
394
443
  const deltaY = touch.pageY - (this.touchStartY ?? touch.pageY);
395
- if (Math.abs(deltaY) >= 8 && Math.abs(deltaY) > Math.abs(deltaX)) this.cancelPendingTopFrame();
444
+ if (Math.abs(deltaY) >= 8 && Math.abs(deltaY) > Math.abs(deltaX)) this.takeOverVerticalScroll();
396
445
  if (Math.abs(deltaX) >= 8 && Math.abs(deltaX) > Math.abs(deltaY)) {
397
446
  this.cancelPendingTopFrame();
398
447
  this.horizontalTouch = true;
@@ -59,6 +59,8 @@ export declare class CollapsiblePagerView extends React.PureComponent<Collapsibl
59
59
  private pageOffsets;
60
60
  private pageHosts;
61
61
  private sharedHeaderOffset;
62
+ private selectionRestoreToken;
63
+ private bodyAlignment;
62
64
  private scrollEnabled;
63
65
  private lastMountSignature;
64
66
  private configureFrame;
@@ -80,6 +82,7 @@ export declare class CollapsiblePagerView extends React.PureComponent<Collapsibl
80
82
  }>) => void;
81
83
  private cancelPendingTopFrame;
82
84
  private onQueuedTopWheel;
85
+ private onScrollbarPointerDown;
83
86
  private onQueuedTopKey;
84
87
  private scrollCurrentPageToTop;
85
88
  setScrollEnabled: (scrollEnabled: boolean) => void;
@@ -97,6 +100,8 @@ export declare class CollapsiblePagerView extends React.PureComponent<Collapsibl
97
100
  private restoreConfiguredScrollElement;
98
101
  private configureActiveScrollElement;
99
102
  private scheduleScrollConfiguration;
103
+ private takeOverVerticalScroll;
104
+ private onListScrollCommandAccepted;
100
105
  private onVerticalScrollSettled;
101
106
  private savePageOffset;
102
107
  private restorePageOffset;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-pager-view",
3
- "version": "3.0.175",
3
+ "version": "3.0.176",
4
4
  "description": "React Native wrapper for Android and iOS ViewPager",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
@@ -72,7 +72,7 @@
72
72
  "typescript": "^5.9.2"
73
73
  },
74
74
  "peerDependencies": {
75
- "@onekeyfe/react-native-native-logger": "3.0.175",
75
+ "@onekeyfe/react-native-native-logger": "3.0.176",
76
76
  "react": "*",
77
77
  "react-native": "*"
78
78
  },
@@ -154,6 +154,11 @@ export class CollapsiblePagerView extends React.PureComponent<
154
154
  private pageOffsets = new Map<string, number>();
155
155
  private pageHosts = new Map<number, HTMLElement>();
156
156
  private sharedHeaderOffset = 0;
157
+ private selectionRestoreToken = 0;
158
+ private bodyAlignment: Readonly<{
159
+ element: HTMLElement;
160
+ key: string;
161
+ }> | null = null;
157
162
  private scrollEnabled = this.props.scrollEnabled ?? true;
158
163
  private lastMountSignature: string | null = null;
159
164
  private configureFrame: number | null = null;
@@ -188,6 +193,7 @@ export class CollapsiblePagerView extends React.PureComponent<
188
193
  const pageCount = this.pages().length;
189
194
  const selectedPage = clampedPageIndex(this.state.selectedPage, pageCount);
190
195
  if (selectedPage !== this.state.selectedPage) {
196
+ this.selectionRestoreToken += 1;
191
197
  this.cancelPendingTopFrame();
192
198
  this.finishPageTransition();
193
199
  this.setState({
@@ -201,6 +207,7 @@ export class CollapsiblePagerView extends React.PureComponent<
201
207
  }
202
208
 
203
209
  componentWillUnmount() {
210
+ this.selectionRestoreToken += 1;
204
211
  this.cancelPendingTopFrame();
205
212
  this.savePageOffset(this.state.selectedPage);
206
213
  if (this.configureFrame !== null) {
@@ -247,7 +254,33 @@ export class CollapsiblePagerView extends React.PureComponent<
247
254
  };
248
255
 
249
256
  private onQueuedTopWheel = (event: WheelEvent) => {
250
- if (event.deltaY !== 0) this.cancelPendingTopFrame();
257
+ if (event.deltaY !== 0) this.takeOverVerticalScroll();
258
+ };
259
+
260
+ private onScrollbarPointerDown = (event: PointerEvent) => {
261
+ const element = this.pageScrollElement(this.state.selectedPage);
262
+ if (!element || event.target !== element || event.button !== 0) return;
263
+ const rect = element.getBoundingClientRect();
264
+ const computed =
265
+ element.ownerDocument.defaultView?.getComputedStyle(element);
266
+ const borderLeft = Number.parseFloat(computed?.borderLeftWidth ?? "0") || 0;
267
+ const borderRight =
268
+ Number.parseFloat(computed?.borderRightWidth ?? "0") || 0;
269
+ const clientStart = rect.left + element.clientLeft;
270
+ const clientEnd = clientStart + element.clientWidth;
271
+ const leftGutter = clientStart - (rect.left + borderLeft);
272
+ const rightGutter = rect.right - borderRight - clientEnd;
273
+ const inVerticalTrack =
274
+ event.clientY >= rect.top + element.clientTop &&
275
+ event.clientY < rect.top + element.clientTop + element.clientHeight;
276
+ const inGutter =
277
+ (leftGutter > 0 &&
278
+ event.clientX >= rect.left + borderLeft &&
279
+ event.clientX < clientStart) ||
280
+ (rightGutter > 0 &&
281
+ event.clientX >= clientEnd &&
282
+ event.clientX < rect.right - borderRight);
283
+ if (inVerticalTrack && inGutter) this.takeOverVerticalScroll();
251
284
  };
252
285
 
253
286
  private onQueuedTopKey = (event: KeyboardEvent) => {
@@ -262,7 +295,7 @@ export class CollapsiblePagerView extends React.PureComponent<
262
295
  " ",
263
296
  ].includes(event.key)
264
297
  )
265
- this.cancelPendingTopFrame();
298
+ this.takeOverVerticalScroll();
266
299
  };
267
300
 
268
301
  private scrollCurrentPageToTop(offset: number, animated: boolean) {
@@ -270,6 +303,7 @@ export class CollapsiblePagerView extends React.PureComponent<
270
303
  const element = this.pageScrollElement(this.state.selectedPage);
271
304
  const key = this.pages().map(pageKey)[this.state.selectedPage];
272
305
  if (!element?.isConnected || !key) return;
306
+ this.selectionRestoreToken += 1;
273
307
  this.cancelPendingTopFrame();
274
308
  const EventConstructor = element.ownerDocument.defaultView?.Event;
275
309
  if (EventConstructor)
@@ -292,8 +326,9 @@ export class CollapsiblePagerView extends React.PureComponent<
292
326
  this.pages().map(pageKey)[this.state.selectedPage] !== key
293
327
  )
294
328
  return;
295
- this.pageOffsets.set(key, offset);
329
+ this.pageOffsets.set(key, 0);
296
330
  this.sharedHeaderOffset = offset;
331
+ this.bodyAlignment = { element, key };
297
332
  element.scrollTo({
298
333
  top: offset,
299
334
  behavior: animated ? "smooth" : "instant",
@@ -392,9 +427,13 @@ export class CollapsiblePagerView extends React.PureComponent<
392
427
  this.cancelPendingTopFrame();
393
428
  configured.element.removeEventListener(
394
429
  "ok-native-list-scroll-command-accepted",
395
- this.cancelPendingTopFrame
430
+ this.onListScrollCommandAccepted
396
431
  );
397
432
  configured.element.removeEventListener("wheel", this.onQueuedTopWheel);
433
+ configured.element.removeEventListener(
434
+ "pointerdown",
435
+ this.onScrollbarPointerDown
436
+ );
398
437
  configured.element.removeEventListener("keydown", this.onQueuedTopKey);
399
438
  configured.element.removeEventListener(
400
439
  "scrollend",
@@ -412,6 +451,8 @@ export class CollapsiblePagerView extends React.PureComponent<
412
451
  configured.element.removeAttribute("data-collapsible-pager-active");
413
452
  this.notifyInsetsChanged(configured.element);
414
453
  this.configuredScrollElement = null;
454
+ if (this.bodyAlignment?.element === configured.element)
455
+ this.bodyAlignment = null;
415
456
  }
416
457
 
417
458
  private configureActiveScrollElement = () => {
@@ -458,9 +499,10 @@ export class CollapsiblePagerView extends React.PureComponent<
458
499
  element.addEventListener("scrollend", this.onVerticalScrollSettled);
459
500
  element.addEventListener(
460
501
  "ok-native-list-scroll-command-accepted",
461
- this.cancelPendingTopFrame
502
+ this.onListScrollCommandAccepted
462
503
  );
463
504
  element.addEventListener("wheel", this.onQueuedTopWheel);
505
+ element.addEventListener("pointerdown", this.onScrollbarPointerDown);
464
506
  element.addEventListener("keydown", this.onQueuedTopKey);
465
507
  }
466
508
 
@@ -501,8 +543,26 @@ export class CollapsiblePagerView extends React.PureComponent<
501
543
  }
502
544
  }
503
545
 
504
- private onVerticalScrollSettled = () => {
505
- if (this.pendingTopFrame) return;
546
+ private takeOverVerticalScroll = () => {
547
+ this.selectionRestoreToken += 1;
548
+ this.bodyAlignment = null;
549
+ this.cancelPendingTopFrame();
550
+ this.savePageOffset(this.state.selectedPage);
551
+ };
552
+
553
+ private onListScrollCommandAccepted = (event: Event) => {
554
+ if (event.target !== this.pageScrollElement(this.state.selectedPage))
555
+ return;
556
+ this.takeOverVerticalScroll();
557
+ };
558
+
559
+ private onVerticalScrollSettled = (event?: Event) => {
560
+ if (
561
+ this.pendingTopFrame ||
562
+ (event &&
563
+ event.target !== this.pageScrollElement(this.state.selectedPage))
564
+ )
565
+ return;
506
566
  this.savePageOffset(this.state.selectedPage);
507
567
  this.emitDiagnostics("scroll-settled");
508
568
  };
@@ -511,10 +571,16 @@ export class CollapsiblePagerView extends React.PureComponent<
511
571
  const key = this.pages().map(pageKey)[index];
512
572
  const scrollElement = this.pageScrollElement(index);
513
573
  if (key && scrollElement) {
514
- this.pageOffsets.set(key, scrollElement.scrollTop);
574
+ if (
575
+ this.bodyAlignment?.key === key &&
576
+ this.bodyAlignment.element === scrollElement
577
+ )
578
+ return;
579
+ const D = Math.max(0, this.props.headerHeight);
580
+ this.pageOffsets.set(key, Math.max(0, scrollElement.scrollTop - D));
515
581
  this.sharedHeaderOffset = Math.min(
516
582
  Math.max(scrollElement.scrollTop, 0),
517
- Math.max(0, this.props.headerHeight)
583
+ D
518
584
  );
519
585
  }
520
586
  }
@@ -523,10 +589,11 @@ export class CollapsiblePagerView extends React.PureComponent<
523
589
  const key = this.pages().map(pageKey)[index];
524
590
  const scrollElement = this.pageScrollElement(index);
525
591
  if (!key || !scrollElement) return;
526
- scrollElement.scrollTop = Math.max(
527
- this.pageOffsets.get(key) ?? this.sharedHeaderOffset,
528
- this.sharedHeaderOffset
529
- );
592
+ const D = Math.max(0, this.props.headerHeight);
593
+ const C = Math.min(Math.max(0, this.sharedHeaderOffset), D);
594
+ const expectedQ = C + (C === D ? this.pageOffsets.get(key) ?? 0 : 0);
595
+ this.bodyAlignment = { element: scrollElement, key };
596
+ scrollElement.scrollTop = expectedQ;
530
597
  }
531
598
 
532
599
  private selectPage(selectedPage: number, animated: boolean) {
@@ -540,6 +607,8 @@ export class CollapsiblePagerView extends React.PureComponent<
540
607
  ) {
541
608
  return;
542
609
  }
610
+ const selectionToken = ++this.selectionRestoreToken;
611
+ const expectedKey = this.pages().map(pageKey)[position];
543
612
  this.cancelPendingTopFrame();
544
613
  this.savePageOffset(this.state.selectedPage);
545
614
 
@@ -550,6 +619,13 @@ export class CollapsiblePagerView extends React.PureComponent<
550
619
  () => {
551
620
  this.scheduleScrollConfiguration();
552
621
  const restore = () => {
622
+ if (
623
+ this.selectionRestoreToken !== selectionToken ||
624
+ this.state.selectedPage !== position ||
625
+ this.pages().map(pageKey)[position] !== expectedKey ||
626
+ !this.pageScrollElement(position)?.isConnected
627
+ )
628
+ return;
553
629
  this.configureActiveScrollElement();
554
630
  this.restorePageOffset(position);
555
631
  };
@@ -629,7 +705,7 @@ export class CollapsiblePagerView extends React.PureComponent<
629
705
  const deltaX = touch.pageX - this.touchStartX;
630
706
  const deltaY = touch.pageY - (this.touchStartY ?? touch.pageY);
631
707
  if (Math.abs(deltaY) >= 8 && Math.abs(deltaY) > Math.abs(deltaX))
632
- this.cancelPendingTopFrame();
708
+ this.takeOverVerticalScroll();
633
709
  if (Math.abs(deltaX) >= 8 && Math.abs(deltaX) > Math.abs(deltaY)) {
634
710
  this.cancelPendingTopFrame();
635
711
  this.horizontalTouch = true;