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

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 (42) hide show
  1. package/README.md +14 -0
  2. package/android/src/androidTest/AndroidManifest.xml +7 -0
  3. package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +306 -0
  4. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +90 -21
  5. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +72 -7
  6. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +27 -0
  7. package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +19 -0
  8. package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +50 -6
  9. package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +40 -0
  10. package/docs/SPEC.md +253 -10
  11. package/ios/RNCCollapsiblePagerViewComponentView.mm +614 -42
  12. package/ios/RNCNativeScrollerComponentView.h +2 -0
  13. package/ios/RNCNativeScrollerComponentView.mm +42 -0
  14. package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +393 -0
  15. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +338 -0
  16. package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +74 -9
  17. package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +50 -0
  18. package/lib/module/CollapsiblePagerView.js +32 -7
  19. package/lib/module/CollapsiblePagerView.web.js +439 -39
  20. package/lib/module/CollapsiblePagerViewNativeComponent.ts +13 -0
  21. package/lib/module/NativeScroller.web.js +16 -3
  22. package/lib/module/WebCollapsiblePagerPage.js +182 -0
  23. package/lib/module/index.js +2 -0
  24. package/lib/module/index.native.js +9 -0
  25. package/lib/module/index.web.js +2 -0
  26. package/lib/typescript/src/CollapsiblePagerView.d.ts +10 -3
  27. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +66 -3
  28. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +5 -0
  29. package/lib/typescript/src/WebCollapsiblePagerPage.d.ts +42 -0
  30. package/lib/typescript/src/index.d.ts +4 -0
  31. package/lib/typescript/src/index.native.d.ts +13 -0
  32. package/lib/typescript/src/index.web.d.ts +4 -0
  33. package/package.json +6 -4
  34. package/react-native-pager-view.podspec +1 -0
  35. package/src/CollapsiblePagerView.tsx +99 -8
  36. package/src/CollapsiblePagerView.web.tsx +657 -51
  37. package/src/CollapsiblePagerViewNativeComponent.ts +13 -0
  38. package/src/NativeScroller.web.tsx +24 -2
  39. package/src/WebCollapsiblePagerPage.tsx +258 -0
  40. package/src/index.native.tsx +30 -0
  41. package/src/index.tsx +4 -0
  42. package/src/index.web.tsx +4 -0
@@ -1,8 +1,11 @@
1
1
  import type { ScrollEdgeEffect } from "./ScrollEdgeEffect";
2
2
  import React from "react";
3
- import { I18nManager, StyleSheet, View } from "react-native";
3
+ import { I18nManager, Platform, StyleSheet, View } from "react-native";
4
4
  import type * as ReactNative from "react-native";
5
5
 
6
+ import { WebCollapsiblePagerPage } from "./WebCollapsiblePagerPage";
7
+ import type { WebCollapsiblePagerPageBridge } from "./WebCollapsiblePagerPage";
8
+
6
9
  export type OnPageScrollEventData = Readonly<{
7
10
  position: number;
8
11
  offset: number;
@@ -65,8 +68,9 @@ export interface CollapsiblePagerViewProps
65
68
  > {
66
69
  header: React.ReactNode;
67
70
  stickyHeader: React.ReactNode;
68
- headerHeight: number;
69
- stickyHeaderHeight: number;
71
+ headerHeight?: number;
72
+ stickyHeaderHeight?: number;
73
+ isActive?: boolean;
70
74
  topOverlay?: React.ReactNode;
71
75
  topScrollEdgeEffect?: `${ScrollEdgeEffect}`;
72
76
  topOverlayColorScheme?: "light" | "dark";
@@ -83,7 +87,9 @@ type State = {
83
87
  };
84
88
 
85
89
  type ConfiguredScrollElement = Readonly<{
90
+ key: string;
86
91
  element: HTMLElement;
92
+ paddingTarget: HTMLElement;
87
93
  rangeSpacer: HTMLElement;
88
94
  position: string;
89
95
  paddingTop: string;
@@ -95,6 +101,14 @@ type ConfiguredScrollElement = Readonly<{
95
101
  pagerStickyInset: string;
96
102
  }>;
97
103
 
104
+ type WebRouteTarget = Readonly<{
105
+ key: string;
106
+ identity: string;
107
+ owner: HTMLElement;
108
+ generation: number;
109
+ top: number;
110
+ }>;
111
+
98
112
  const COLLAPSE_ANIMATION_NAME = "ok-collapsible-pager-collapse";
99
113
  const COLLAPSE_STYLE_ID = "ok-collapsible-pager-styles";
100
114
  const PAGER_HEADER_INSET = "--ok-collapsible-pager-header-inset";
@@ -124,6 +138,14 @@ const timelineStyle = (element: HTMLElement) =>
124
138
  scrollTimelineAxis: string;
125
139
  };
126
140
 
141
+ function scrollElementTo(element: HTMLElement, options: ScrollToOptions) {
142
+ // RN Web decorates ScrollView nodes with a React Native scrollTo signature.
143
+ const scrollTo: (options: ScrollToOptions) => void =
144
+ element.ownerDocument.defaultView?.Element.prototype.scrollTo ??
145
+ element.scrollTo;
146
+ scrollTo.call(element, options);
147
+ }
148
+
127
149
  function ensureCollapseAnimation(document: Document) {
128
150
  if (document.getElementById(COLLAPSE_STYLE_ID)) return;
129
151
  const style = document.createElement("style");
@@ -132,6 +154,19 @@ function ensureCollapseAnimation(document: Document) {
132
154
  document.head.appendChild(style);
133
155
  }
134
156
 
157
+ function supportsScrollTimeline(document: Document) {
158
+ const css = document.defaultView?.CSS;
159
+ return !!(
160
+ typeof css?.supports === "function" &&
161
+ css.supports("animation-timeline", "--ok-test") &&
162
+ css.supports("timeline-scope", "--ok-test") &&
163
+ css.supports("scroll-timeline-name", "--ok-test") &&
164
+ css.supports("scroll-timeline-axis", "block") &&
165
+ css.supports("animation-range", "0px 1px") &&
166
+ css.supports("animation-duration", "auto")
167
+ );
168
+ }
169
+
135
170
  const pageKey = (child: React.ReactNode, index: number) => {
136
171
  if (React.isValidElement(child) && child.key != null)
137
172
  return String(child.key);
@@ -153,11 +188,39 @@ export class CollapsiblePagerView extends React.PureComponent<
153
188
 
154
189
  private root: HTMLElement | null = null;
155
190
  private track: HTMLElement | null = null;
191
+ private headerSlot: HTMLElement | null = null;
192
+ private stickySlot: HTMLElement | null = null;
193
+ private cssTimelineSupported = false;
194
+ private overlayTouch: {
195
+ owner: HTMLElement;
196
+ target: HTMLElement;
197
+ x: number;
198
+ y: number;
199
+ lastY: number;
200
+ } | null = null;
156
201
  private touchStartX: number | null = null;
157
202
  private touchStartY: number | null = null;
158
203
  private horizontalTouch = false;
159
204
  private pageOffsets = new Map<string, number>();
160
205
  private pageHosts = new Map<number, HTMLElement>();
206
+ private pageScrollIdentities = new Map<string, string>();
207
+ private pageBridges = new Map<string, WebCollapsiblePagerPageBridge>();
208
+ private ownerRecords = new Map<
209
+ string,
210
+ { element: HTMLElement; identity: string; generation: number }
211
+ >();
212
+ private ownerGeneration = 0;
213
+ private lastRawTops = new Map<string, number>();
214
+ private routeTarget: WebRouteTarget | null = null;
215
+ private measuredHeaderHeight = 0;
216
+ private measuredStickyHeight = 0;
217
+ private headerMeasured = false;
218
+ private stickyMeasured = false;
219
+ private headerContent: HTMLElement | null = null;
220
+ private stickyContent: HTMLElement | null = null;
221
+ private chromeObserver: ResizeObserver | null = null;
222
+ private chromeMeasureFrame: number | null = null;
223
+ private disposed = false;
161
224
  private sharedHeaderOffset = 0;
162
225
  private selectionRestoreToken = 0;
163
226
  private bodyAlignment: Readonly<{
@@ -178,14 +241,79 @@ export class CollapsiblePagerView extends React.PureComponent<
178
241
  private configuredScrollElement: ConfiguredScrollElement | null = null;
179
242
  private readonly timelineName = `--ok-collapsible-pager-${++collapsiblePagerWebInstance}`;
180
243
 
244
+ private get headerHeight() {
245
+ return this.hasExplicitHeaderHeight
246
+ ? (this.props.headerHeight ?? this.measuredHeaderHeight)
247
+ : this.measuredHeaderHeight;
248
+ }
249
+
250
+ private get hasExplicitHeaderHeight() {
251
+ return this.props.headerHeight !== undefined &&
252
+ Number.isFinite(this.props.headerHeight) &&
253
+ this.props.headerHeight >= 0;
254
+ }
255
+
256
+ private get stickyHeaderHeight() {
257
+ return this.hasExplicitStickyHeaderHeight
258
+ ? (this.props.stickyHeaderHeight ?? this.measuredStickyHeight)
259
+ : this.measuredStickyHeight;
260
+ }
261
+
262
+ private get hasExplicitStickyHeaderHeight() {
263
+ return this.props.stickyHeaderHeight !== undefined &&
264
+ Number.isFinite(this.props.stickyHeaderHeight) &&
265
+ this.props.stickyHeaderHeight >= 0;
266
+ }
267
+
268
+ getSnapshotBeforeUpdate(previousProps: CollapsiblePagerViewProps) {
269
+ if (previousProps.isActive !== false && this.props.isActive === false)
270
+ return this.captureRouteTarget(previousProps);
271
+ return null;
272
+ }
273
+
181
274
  componentDidMount() {
182
- if (this.root) ensureCollapseAnimation(this.root.ownerDocument);
275
+ this.disposed = false;
276
+ if (Platform.OS !== "web" || typeof document === "undefined")
277
+ throw new Error("WebCollapsiblePagerView requires a Web DOM runtime");
278
+ if (!this.hasExplicitHeaderHeight) this.headerMeasured = false;
279
+ if (!this.hasExplicitStickyHeaderHeight) this.stickyMeasured = false;
280
+ this.observeChrome();
281
+ if (this.root) {
282
+ this.cssTimelineSupported = supportsScrollTimeline(
283
+ this.root.ownerDocument
284
+ );
285
+ if (this.cssTimelineSupported) {
286
+ ensureCollapseAnimation(this.root.ownerDocument);
287
+ this.forceUpdate();
288
+ }
289
+ }
183
290
  this.scheduleScrollConfiguration();
291
+ this.syncFallbackHeader();
184
292
  this.notifyMountedPagesChanged();
185
293
  this.emitDiagnostics("mounted");
186
294
  }
187
295
 
188
- componentDidUpdate(previousProps: CollapsiblePagerViewProps) {
296
+ componentDidUpdate(
297
+ previousProps: CollapsiblePagerViewProps,
298
+ _previousState: State,
299
+ routeSnapshot: WebRouteTarget | null,
300
+ ) {
301
+ if (routeSnapshot) {
302
+ const record = this.ownerRecords.get(routeSnapshot.key);
303
+ if (
304
+ this.pages().map(pageKey).includes(routeSnapshot.key) &&
305
+ record?.element === routeSnapshot.owner &&
306
+ record.identity === routeSnapshot.identity &&
307
+ record.generation === routeSnapshot.generation
308
+ )
309
+ this.setRouteTarget(routeSnapshot);
310
+ }
311
+ if (previousProps.isActive === false && this.props.isActive !== false) {
312
+ if (!this.hasExplicitHeaderHeight) this.headerMeasured = false;
313
+ if (!this.hasExplicitStickyHeaderHeight) this.stickyMeasured = false;
314
+ this.scheduleChromeMeasurement();
315
+ this.restoreScrollPosition();
316
+ }
189
317
  if (previousProps.scrollEnabled !== this.props.scrollEnabled) {
190
318
  this.scrollEnabled = this.props.scrollEnabled ?? true;
191
319
  if (!this.scrollEnabled) {
@@ -207,11 +335,37 @@ export class CollapsiblePagerView extends React.PureComponent<
207
335
  });
208
336
  return;
209
337
  }
338
+ // Child layout effects can run before page-host refs are reattached.
339
+ // Recheck identities in the same commit, before a new user command runs.
340
+ this.mountedPages().forEach((index) =>
341
+ this.synchronizeScrollOwnerIdentity(index)
342
+ );
343
+ const currentKeys = new Set(this.pages().map(pageKey));
344
+ const ownedKeys = new Set([
345
+ ...this.pageScrollIdentities.keys(),
346
+ ...this.pageOffsets.keys(),
347
+ ...this.lastRawTops.keys(),
348
+ ...this.ownerRecords.keys(),
349
+ ]);
350
+ for (const key of ownedKeys) {
351
+ if (!currentKeys.has(key)) {
352
+ this.pageScrollIdentities.delete(key);
353
+ this.pageOffsets.delete(key);
354
+ this.lastRawTops.delete(key);
355
+ this.ownerRecords.delete(key);
356
+ if (this.routeTarget?.key === key) this.setRouteTarget(null);
357
+ }
358
+ }
210
359
  this.scheduleScrollConfiguration();
360
+ this.syncFallbackHeader();
211
361
  this.notifyMountedPagesChanged();
212
362
  }
213
363
 
214
364
  componentWillUnmount() {
365
+ // Activity can disconnect effects before the inactive update commits.
366
+ const routeTarget = this.captureRouteTarget(this.props);
367
+ if (routeTarget) this.setRouteTarget(routeTarget);
368
+ this.disposed = true;
215
369
  this.selectionRestoreToken += 1;
216
370
  this.cancelPendingTopFrame();
217
371
  this.savePageOffset(this.state.selectedPage);
@@ -224,9 +378,196 @@ export class CollapsiblePagerView extends React.PureComponent<
224
378
  this.transitionTimer = null;
225
379
  }
226
380
  this.setTrack(null);
381
+ this.setRoot(null);
382
+ this.chromeObserver?.disconnect();
383
+ this.chromeObserver = null;
384
+ if (this.chromeMeasureFrame !== null) {
385
+ cancelAnimationFrame(this.chromeMeasureFrame);
386
+ this.chromeMeasureFrame = null;
387
+ }
388
+ this.pageBridges.clear();
227
389
  this.restoreConfiguredScrollElement();
228
390
  }
229
391
 
392
+ private setHeaderContent = (node: unknown) => {
393
+ const element = webElement(node);
394
+ if (element === this.headerContent) return;
395
+ if (this.headerContent) this.chromeObserver?.unobserve(this.headerContent);
396
+ this.headerContent = element;
397
+ this.headerMeasured = false;
398
+ if (element) this.chromeObserver?.observe(element);
399
+ this.scheduleChromeMeasurement();
400
+ };
401
+
402
+ private setStickyContent = (node: unknown) => {
403
+ const element = webElement(node);
404
+ if (element === this.stickyContent) return;
405
+ if (this.stickyContent) this.chromeObserver?.unobserve(this.stickyContent);
406
+ this.stickyContent = element;
407
+ this.stickyMeasured = false;
408
+ if (element) this.chromeObserver?.observe(element);
409
+ this.scheduleChromeMeasurement();
410
+ };
411
+
412
+ private observeChrome() {
413
+ this.chromeObserver = new ResizeObserver(this.scheduleChromeMeasurement);
414
+ if (this.headerContent) this.chromeObserver.observe(this.headerContent);
415
+ if (this.stickyContent) this.chromeObserver.observe(this.stickyContent);
416
+ this.scheduleChromeMeasurement();
417
+ }
418
+
419
+ private scheduleChromeMeasurement = () => {
420
+ if (this.disposed || this.chromeMeasureFrame !== null || typeof requestAnimationFrame !== "function")
421
+ return;
422
+ this.chromeMeasureFrame = requestAnimationFrame(() => {
423
+ this.chromeMeasureFrame = null;
424
+ const header = this.headerContent;
425
+ const sticky = this.stickyContent;
426
+ const nextHeader = header?.isConnected
427
+ ? Math.max(0, header.getBoundingClientRect().height)
428
+ : 0;
429
+ const nextSticky = sticky?.isConnected
430
+ ? Math.max(0, sticky.getBoundingClientRect().height)
431
+ : 0;
432
+ const headerChanged =
433
+ this.measuredHeaderHeight !== nextHeader ||
434
+ this.headerMeasured !== Boolean(header?.isConnected);
435
+ const stickyChanged =
436
+ this.measuredStickyHeight !== nextSticky ||
437
+ this.stickyMeasured !== Boolean(sticky?.isConnected);
438
+ this.measuredHeaderHeight = nextHeader;
439
+ this.measuredStickyHeight = nextSticky;
440
+ this.headerMeasured = Boolean(header?.isConnected);
441
+ this.stickyMeasured = Boolean(sticky?.isConnected);
442
+ if (headerChanged || stickyChanged) {
443
+ this.forceUpdate();
444
+ this.scheduleScrollConfiguration();
445
+ }
446
+ });
447
+ };
448
+
449
+ private onPageBridge = (key: string, bridge: WebCollapsiblePagerPageBridge | null) => {
450
+ if (bridge) {
451
+ this.pageBridges.set(key, bridge);
452
+ bridge.setPendingRouteScrollTop(
453
+ this.routeTarget?.key === key ? this.routeTarget.top : null,
454
+ );
455
+ const index = this.pages().map(pageKey).indexOf(key);
456
+ if (index >= 0) this.syncPageOwner(index);
457
+ } else {
458
+ this.pageBridges.delete(key);
459
+ }
460
+ };
461
+
462
+ private setRouteTarget(target: WebRouteTarget | null) {
463
+ const oldKey = this.routeTarget?.key;
464
+ this.routeTarget = target;
465
+ if (oldKey) this.pageBridges.get(oldKey)?.setPendingRouteScrollTop(null);
466
+ if (target)
467
+ this.pageBridges.get(target.key)?.setPendingRouteScrollTop(target.top);
468
+ }
469
+
470
+ private syncPageOwner(index: number) {
471
+ const key = this.pages().map(pageKey)[index];
472
+ const element = this.pageScrollElement(index);
473
+ if (!key || !element) return;
474
+ const identity = element.getAttribute("data-collapsible-pager-scroll-key") ?? key;
475
+ const previous = this.ownerRecords.get(key);
476
+ if (previous?.element !== element || previous.identity !== identity) {
477
+ if (this.routeTarget?.key === key) this.setRouteTarget(null);
478
+ this.ownerRecords.set(key, {
479
+ element,
480
+ identity,
481
+ generation: ++this.ownerGeneration,
482
+ });
483
+ this.lastRawTops.delete(key);
484
+ }
485
+ this.pageBridges.get(key)?.setOwner(element);
486
+ }
487
+
488
+ private captureRouteTarget(previousProps: CollapsiblePagerViewProps) {
489
+ const index = this.state.selectedPage;
490
+ const oldKey = React.Children.toArray(previousProps.children).map(pageKey)[index];
491
+ const key = this.configuredScrollElement?.key ?? oldKey;
492
+ const record = key ? this.ownerRecords.get(key) : null;
493
+ if (!record?.element.isConnected) return null;
494
+ if (
495
+ this.routeTarget?.key === key &&
496
+ this.routeTarget.identity === record.identity &&
497
+ this.routeTarget.owner === record.element &&
498
+ this.routeTarget.generation === record.generation
499
+ )
500
+ return this.routeTarget;
501
+ const top =
502
+ record.element.clientHeight > 0
503
+ ? record.element.scrollTop
504
+ : this.lastRawTops.get(key) ?? record.element.scrollTop;
505
+ return {
506
+ key,
507
+ identity: record.identity,
508
+ owner: record.element,
509
+ generation: record.generation,
510
+ top: Math.max(0, top),
511
+ };
512
+ }
513
+
514
+ public restoreScrollPosition = () => {
515
+ if (this.disposed || this.props.isActive === false) return false;
516
+ const pending = this.routeTarget;
517
+ if (!pending) return true;
518
+ const key = this.pages().map(pageKey)[this.state.selectedPage];
519
+ if (key !== pending.key) return false;
520
+ this.syncPageOwner(this.state.selectedPage);
521
+ const record = this.ownerRecords.get(key);
522
+ if (
523
+ !record ||
524
+ record.element !== pending.owner ||
525
+ record.identity !== pending.identity ||
526
+ record.generation !== pending.generation
527
+ )
528
+ return false;
529
+ const element = record.element;
530
+ const configured = this.configuredScrollElement;
531
+ if (
532
+ !element.isConnected ||
533
+ element.clientHeight <= 0 ||
534
+ configured?.element !== element ||
535
+ configured.key !== key ||
536
+ (!this.hasExplicitHeaderHeight && !this.headerMeasured) ||
537
+ (!this.hasExplicitStickyHeaderHeight && !this.stickyMeasured) ||
538
+ element.getAttribute("data-collapsible-pager-active") !== "true" ||
539
+ element.style.getPropertyValue(PAGER_HEADER_INSET).trim() !==
540
+ `${this.headerHeight}px` ||
541
+ element.style.getPropertyValue(PAGER_STICKY_INSET).trim() !==
542
+ `${this.stickyHeaderHeight}px` ||
543
+ element.scrollHeight - element.clientHeight < pending.top
544
+ )
545
+ return false;
546
+ this.selectionRestoreToken += 1;
547
+ this.cancelPendingTopFrame();
548
+ this.setRouteTarget(null);
549
+ this.lastRawTops.set(key, pending.top);
550
+ const D = this.headerHeight;
551
+ this.sharedHeaderOffset = Math.min(pending.top, D);
552
+ this.pageOffsets.set(key, Math.max(0, pending.top - D));
553
+ this.bodyAlignment = { element, key };
554
+ scrollElementTo(element, { top: pending.top, behavior: "instant" });
555
+ this.syncFallbackHeader();
556
+ return true;
557
+ };
558
+
559
+ private onPageScroll = (key: string, top: number) => {
560
+ if (this.props.isActive !== false) this.lastRawTops.set(key, top);
561
+ };
562
+
563
+ private onPageGeometryChange = (key: string) => {
564
+ if (this.routeTarget?.key === key) this.restoreScrollPosition();
565
+ };
566
+
567
+ private onPageUserInput = (key: string) => {
568
+ if (this.routeTarget?.key === key) this.setRouteTarget(null);
569
+ };
570
+
230
571
  public setPage = (selectedPage: number) => {
231
572
  this.selectPage(selectedPage, true);
232
573
  };
@@ -239,9 +580,26 @@ export class CollapsiblePagerView extends React.PureComponent<
239
580
  this.scrollCurrentPageToTop(0, options?.animated ?? true);
240
581
  };
241
582
 
583
+ public scrollHeaderBy = (delta: number) => {
584
+ if (!Number.isFinite(delta)) return;
585
+ const offset = Math.min(
586
+ this.headerHeight,
587
+ Math.max(0, this.sharedHeaderOffset + delta)
588
+ );
589
+ this.scrollCurrentPageToTop(offset, false);
590
+ };
591
+
592
+ public scrollHeaderToOffset = (offset: number) => {
593
+ if (!Number.isFinite(offset)) return;
594
+ this.scrollCurrentPageToTop(
595
+ Math.min(this.headerHeight, Math.max(0, offset)),
596
+ false
597
+ );
598
+ };
599
+
242
600
  public scrollToStickyTop = (options?: Readonly<{ animated?: boolean }>) => {
243
601
  this.scrollCurrentPageToTop(
244
- Math.max(0, this.props.headerHeight),
602
+ this.headerHeight,
245
603
  options?.animated ?? true
246
604
  );
247
605
  };
@@ -308,6 +666,7 @@ export class CollapsiblePagerView extends React.PureComponent<
308
666
  const element = this.pageScrollElement(this.state.selectedPage);
309
667
  const key = this.pages().map(pageKey)[this.state.selectedPage];
310
668
  if (!element?.isConnected || !key) return;
669
+ this.setRouteTarget(null);
311
670
  this.selectionRestoreToken += 1;
312
671
  this.cancelPendingTopFrame();
313
672
  const EventConstructor = element.ownerDocument.defaultView?.Event;
@@ -315,7 +674,7 @@ export class CollapsiblePagerView extends React.PureComponent<
315
674
  element.dispatchEvent(
316
675
  new EventConstructor("ok-collapsible-pager-cancel-scroll-command")
317
676
  );
318
- element.scrollTo({
677
+ scrollElementTo(element, {
319
678
  left: element.scrollLeft,
320
679
  top: element.scrollTop,
321
680
  behavior: "instant",
@@ -332,12 +691,13 @@ export class CollapsiblePagerView extends React.PureComponent<
332
691
  )
333
692
  return;
334
693
  this.pageOffsets.set(key, 0);
335
- this.sharedHeaderOffset = offset;
694
+ this.sharedHeaderOffset = Math.min(this.headerHeight, Math.max(0, offset));
336
695
  this.bodyAlignment = { element, key };
337
- element.scrollTo({
338
- top: offset,
696
+ scrollElementTo(element, {
697
+ top: this.sharedHeaderOffset,
339
698
  behavior: animated ? "smooth" : "instant",
340
699
  });
700
+ this.syncFallbackHeader();
341
701
  });
342
702
  this.pendingTopFrame = { frame, element, key };
343
703
  }
@@ -411,12 +771,186 @@ export class CollapsiblePagerView extends React.PureComponent<
411
771
  private pageScrollElement(index: number) {
412
772
  const pageHost = this.pageHosts.get(index);
413
773
  return (
414
- pageHost?.querySelector<HTMLElement>(
415
- ".ok-native-list-viewport, [data-collapsible-pager-scroll]"
416
- ) ?? pageHost
774
+ pageHost?.querySelector<HTMLElement>("[data-collapsible-pager-scroll]") ??
775
+ pageHost?.querySelector<HTMLElement>(".ok-native-list-viewport") ??
776
+ pageHost
417
777
  );
418
778
  }
419
779
 
780
+ private setRoot = (node: unknown) => {
781
+ const root = webElement(node);
782
+ if (root === this.root) return;
783
+ this.root?.removeEventListener(
784
+ "ok-collapsible-pager-scroll-key-changed",
785
+ this.onScrollOwnerIdentityChanged
786
+ );
787
+ this.root?.removeEventListener("wheel", this.onOverlayWheel);
788
+ this.root?.removeEventListener("touchstart", this.onOverlayTouchStart);
789
+ this.root?.removeEventListener("touchmove", this.onOverlayTouchMove);
790
+ this.root?.removeEventListener("touchend", this.onOverlayTouchEnd);
791
+ this.root?.removeEventListener("touchcancel", this.onOverlayTouchEnd);
792
+ this.overlayTouch = null;
793
+ this.root = root;
794
+ root?.addEventListener(
795
+ "ok-collapsible-pager-scroll-key-changed",
796
+ this.onScrollOwnerIdentityChanged
797
+ );
798
+ root?.addEventListener("wheel", this.onOverlayWheel, { passive: false });
799
+ root?.addEventListener("touchstart", this.onOverlayTouchStart, {
800
+ passive: true,
801
+ });
802
+ root?.addEventListener("touchmove", this.onOverlayTouchMove, {
803
+ passive: false,
804
+ });
805
+ root?.addEventListener("touchend", this.onOverlayTouchEnd);
806
+ root?.addEventListener("touchcancel", this.onOverlayTouchEnd);
807
+ };
808
+
809
+ private overlayTarget(event: Event) {
810
+ const target = event.target as HTMLElement | null;
811
+ if (
812
+ !target ||
813
+ (!this.headerSlot?.contains(target) &&
814
+ !this.stickySlot?.contains(target)) ||
815
+ target.closest("input,textarea,select,[contenteditable=true]")
816
+ )
817
+ return null;
818
+ return target;
819
+ }
820
+
821
+ private nestedScrollConsumes(target: HTMLElement, delta: number) {
822
+ for (
823
+ let node: HTMLElement | null = target;
824
+ node && node !== this.root;
825
+ node = node.parentElement
826
+ ) {
827
+ const style = node.ownerDocument.defaultView?.getComputedStyle(node);
828
+ if (
829
+ /auto|scroll/.test(style?.overflowY ?? "") &&
830
+ node.scrollHeight > node.clientHeight &&
831
+ (delta < 0
832
+ ? node.scrollTop > 0
833
+ : node.scrollTop < node.scrollHeight - node.clientHeight)
834
+ )
835
+ return true;
836
+ }
837
+ return false;
838
+ }
839
+
840
+ private scrollOverlayOwner(owner: HTMLElement, delta: number, event: Event) {
841
+ if (!event.cancelable) return;
842
+ const EventConstructor = owner.ownerDocument.defaultView?.Event;
843
+ if (EventConstructor)
844
+ owner.dispatchEvent(
845
+ new EventConstructor("ok-collapsible-pager-user-scroll")
846
+ );
847
+ const top = Math.min(
848
+ Math.max(0, owner.scrollTop + delta),
849
+ owner.scrollHeight - owner.clientHeight
850
+ );
851
+ if (top === owner.scrollTop) return;
852
+ this.takeOverVerticalScroll();
853
+ scrollElementTo(owner, { top, behavior: "instant" });
854
+ event.preventDefault();
855
+ }
856
+
857
+ private onOverlayWheel = (event: WheelEvent) => {
858
+ const target = this.overlayTarget(event);
859
+ const owner = this.pageScrollElement(this.state.selectedPage);
860
+ if (
861
+ !target ||
862
+ !owner ||
863
+ event.defaultPrevented ||
864
+ event.ctrlKey ||
865
+ Math.abs(event.deltaX) > Math.abs(event.deltaY)
866
+ )
867
+ return;
868
+ const lineHeight =
869
+ Number.parseFloat(
870
+ target.ownerDocument.defaultView?.getComputedStyle(target).lineHeight ??
871
+ ""
872
+ ) || 16;
873
+ const delta =
874
+ event.deltaY *
875
+ (event.deltaMode === 1
876
+ ? lineHeight
877
+ : event.deltaMode === 2
878
+ ? owner.clientHeight
879
+ : 1);
880
+ if (!this.nestedScrollConsumes(target, delta))
881
+ this.scrollOverlayOwner(owner, delta, event);
882
+ };
883
+
884
+ private onOverlayTouchStart = (event: TouchEvent) => {
885
+ const target = this.overlayTarget(event);
886
+ const owner = this.pageScrollElement(this.state.selectedPage);
887
+ const touch = event.touches.length === 1 ? event.touches[0] : null;
888
+ this.overlayTouch =
889
+ target && owner && touch
890
+ ? {
891
+ owner,
892
+ target,
893
+ x: touch.clientX,
894
+ y: touch.clientY,
895
+ lastY: touch.clientY,
896
+ }
897
+ : null;
898
+ };
899
+
900
+ private onOverlayTouchMove = (event: TouchEvent) => {
901
+ if (event.defaultPrevented) return;
902
+ const pending = this.overlayTouch;
903
+ const touch = event.touches.length === 1 ? event.touches[0] : null;
904
+ if (
905
+ !pending ||
906
+ !touch ||
907
+ pending.owner !== this.pageScrollElement(this.state.selectedPage)
908
+ ) {
909
+ this.overlayTouch = null;
910
+ return;
911
+ }
912
+ const delta = pending.lastY - touch.clientY;
913
+ pending.lastY = touch.clientY;
914
+ if (
915
+ Math.abs(touch.clientY - pending.y) < 8 ||
916
+ Math.abs(touch.clientX - pending.x) >= Math.abs(touch.clientY - pending.y)
917
+ )
918
+ return;
919
+ if (!this.nestedScrollConsumes(pending.target, delta))
920
+ this.scrollOverlayOwner(pending.owner, delta, event);
921
+ };
922
+
923
+ private onOverlayTouchEnd = () => {
924
+ this.overlayTouch = null;
925
+ };
926
+
927
+ private synchronizeScrollOwnerIdentity(index: number) {
928
+ const key = this.pages().map(pageKey)[index];
929
+ const element = this.pageScrollElement(index);
930
+ if (!key || !element) return;
931
+ const identity =
932
+ element.getAttribute("data-collapsible-pager-scroll-key") ?? key;
933
+ const previous = this.pageScrollIdentities.get(key);
934
+ this.pageScrollIdentities.set(key, identity);
935
+ this.syncPageOwner(index);
936
+ if (previous === undefined || previous === identity) return;
937
+ if (index === this.state.selectedPage) {
938
+ this.selectionRestoreToken += 1;
939
+ this.cancelPendingTopFrame();
940
+ this.sharedHeaderOffset = 0;
941
+ this.bodyAlignment = null;
942
+ scrollElementTo(element, { top: 0, behavior: "instant" });
943
+ }
944
+ this.pageOffsets.delete(key);
945
+ }
946
+
947
+ private onScrollOwnerIdentityChanged = (event: Event) => {
948
+ this.pages().forEach((_, index) => {
949
+ if (event.target === this.pageScrollElement(index))
950
+ this.synchronizeScrollOwnerIdentity(index);
951
+ });
952
+ };
953
+
420
954
  private notifyInsetsChanged(element: HTMLElement) {
421
955
  const EventConstructor = element.ownerDocument.defaultView?.Event;
422
956
  if (EventConstructor) {
@@ -444,10 +978,11 @@ export class CollapsiblePagerView extends React.PureComponent<
444
978
  "scrollend",
445
979
  this.onVerticalScrollSettled
446
980
  );
981
+ configured.element.removeEventListener("scroll", this.syncFallbackHeader);
447
982
  const style = timelineStyle(configured.element);
448
983
  configured.rangeSpacer.remove();
449
984
  style.position = configured.position;
450
- style.paddingTop = configured.paddingTop;
985
+ configured.paddingTarget.style.paddingTop = configured.paddingTop;
451
986
  style.boxSizing = configured.boxSizing;
452
987
  style.scrollTimelineName = configured.scrollTimelineName;
453
988
  style.scrollTimelineAxis = configured.scrollTimelineAxis;
@@ -456,6 +991,7 @@ export class CollapsiblePagerView extends React.PureComponent<
456
991
  configured.element.removeAttribute("data-collapsible-pager-active");
457
992
  this.notifyInsetsChanged(configured.element);
458
993
  this.configuredScrollElement = null;
994
+ this.syncFallbackHeader();
459
995
  if (this.bodyAlignment?.element === configured.element)
460
996
  this.bodyAlignment = null;
461
997
  }
@@ -464,13 +1000,19 @@ export class CollapsiblePagerView extends React.PureComponent<
464
1000
  this.configureFrame = null;
465
1001
  const element = this.pageScrollElement(this.state.selectedPage);
466
1002
  if (!element) return;
1003
+ this.synchronizeScrollOwnerIdentity(this.state.selectedPage);
467
1004
  if (this.configuredScrollElement?.element !== element) {
468
1005
  this.restoreConfiguredScrollElement();
469
1006
  const style = timelineStyle(element);
1007
+ const paddingTarget = element.hasAttribute(
1008
+ "data-collapsible-pager-scroll"
1009
+ )
1010
+ ? webElement(element.firstElementChild) ?? element
1011
+ : element;
470
1012
  const computedPaddingTop =
471
- element.ownerDocument.defaultView?.getComputedStyle(element)
1013
+ element.ownerDocument.defaultView?.getComputedStyle(paddingTarget)
472
1014
  .paddingTop ||
473
- style.paddingTop ||
1015
+ paddingTarget.style.paddingTop ||
474
1016
  "0px";
475
1017
  const rangeSpacer = element.ownerDocument.createElement("div");
476
1018
  Object.assign(rangeSpacer.style, {
@@ -490,10 +1032,12 @@ export class CollapsiblePagerView extends React.PureComponent<
490
1032
  style.position = "relative";
491
1033
  element.appendChild(rangeSpacer);
492
1034
  this.configuredScrollElement = {
1035
+ key: this.pages().map(pageKey)[this.state.selectedPage],
493
1036
  element,
1037
+ paddingTarget,
494
1038
  rangeSpacer,
495
1039
  position,
496
- paddingTop: style.paddingTop,
1040
+ paddingTop: paddingTarget.style.paddingTop,
497
1041
  basePaddingTop: computedPaddingTop,
498
1042
  boxSizing: style.boxSizing,
499
1043
  scrollTimelineName: style.scrollTimelineName,
@@ -509,11 +1053,17 @@ export class CollapsiblePagerView extends React.PureComponent<
509
1053
  element.addEventListener("wheel", this.onQueuedTopWheel);
510
1054
  element.addEventListener("pointerdown", this.onScrollbarPointerDown);
511
1055
  element.addEventListener("keydown", this.onQueuedTopKey);
1056
+ if (!this.cssTimelineSupported)
1057
+ element.addEventListener("scroll", this.syncFallbackHeader, {
1058
+ passive: true,
1059
+ });
512
1060
  }
513
1061
 
514
1062
  const style = timelineStyle(element);
515
- const headerInset = Math.max(0, this.props.headerHeight);
516
- const stickyInset = Math.max(0, this.props.stickyHeaderHeight);
1063
+ const paddingStyle =
1064
+ this.configuredScrollElement?.paddingTarget.style ?? style;
1065
+ const headerInset = this.headerHeight;
1066
+ const stickyInset = this.stickyHeaderHeight;
517
1067
  if (this.configuredScrollElement)
518
1068
  this.configuredScrollElement.rangeSpacer.style.height = `calc(100% + ${headerInset}px)`;
519
1069
  const basePaddingTop =
@@ -524,10 +1074,10 @@ export class CollapsiblePagerView extends React.PureComponent<
524
1074
  const headerInsetValue = `${headerInset}px`;
525
1075
  const stickyInsetValue = `${stickyInset}px`;
526
1076
  const insetsChanged =
527
- style.paddingTop !== paddingTop ||
1077
+ paddingStyle.paddingTop !== paddingTop ||
528
1078
  style.getPropertyValue(PAGER_HEADER_INSET) !== headerInsetValue ||
529
1079
  style.getPropertyValue(PAGER_STICKY_INSET) !== stickyInsetValue;
530
- style.paddingTop = paddingTop;
1080
+ paddingStyle.paddingTop = paddingTop;
531
1081
  style.boxSizing = "border-box";
532
1082
  style.scrollTimelineName = this.timelineName;
533
1083
  style.scrollTimelineAxis = "block";
@@ -535,10 +1085,25 @@ export class CollapsiblePagerView extends React.PureComponent<
535
1085
  style.setProperty(PAGER_STICKY_INSET, stickyInsetValue);
536
1086
  element.setAttribute("data-collapsible-pager-active", "true");
537
1087
  if (insetsChanged) this.notifyInsetsChanged(element);
1088
+ this.syncFallbackHeader();
1089
+ this.pageBridges.get(this.pages().map(pageKey)[this.state.selectedPage])?.requestMeasure();
1090
+ this.restoreScrollPosition();
1091
+ };
1092
+
1093
+ private syncFallbackHeader = () => {
1094
+ if (this.cssTimelineSupported) return;
1095
+ const top = this.configuredScrollElement?.element.scrollTop ?? 0;
1096
+ const collapse = Math.min(
1097
+ Math.max(top, 0),
1098
+ this.headerHeight
1099
+ );
1100
+ const transform = `translateY(${-collapse}px)`;
1101
+ if (this.headerSlot) this.headerSlot.style.transform = transform;
1102
+ if (this.stickySlot) this.stickySlot.style.transform = transform;
538
1103
  };
539
1104
 
540
1105
  private scheduleScrollConfiguration() {
541
- if (this.configureFrame !== null) return;
1106
+ if (this.disposed || this.configureFrame !== null) return;
542
1107
  if (typeof requestAnimationFrame === "function") {
543
1108
  this.configureFrame = requestAnimationFrame(
544
1109
  this.configureActiveScrollElement
@@ -549,6 +1114,7 @@ export class CollapsiblePagerView extends React.PureComponent<
549
1114
  }
550
1115
 
551
1116
  private takeOverVerticalScroll = () => {
1117
+ this.setRouteTarget(null);
552
1118
  this.selectionRestoreToken += 1;
553
1119
  this.bodyAlignment = null;
554
1120
  this.cancelPendingTopFrame();
@@ -573,15 +1139,18 @@ export class CollapsiblePagerView extends React.PureComponent<
573
1139
  };
574
1140
 
575
1141
  private savePageOffset(index: number) {
576
- const key = this.pages().map(pageKey)[index];
577
- const scrollElement = this.pageScrollElement(index);
1142
+ const configured =
1143
+ index === this.state.selectedPage ? this.configuredScrollElement : null;
1144
+ const key = configured?.key ?? this.pages().map(pageKey)[index];
1145
+ const scrollElement = configured?.element ?? this.pageScrollElement(index);
1146
+ if (configured && !scrollElement?.isConnected) return;
578
1147
  if (key && scrollElement) {
579
1148
  if (
580
1149
  this.bodyAlignment?.key === key &&
581
1150
  this.bodyAlignment.element === scrollElement
582
1151
  )
583
1152
  return;
584
- const D = Math.max(0, this.props.headerHeight);
1153
+ const D = this.headerHeight;
585
1154
  this.pageOffsets.set(key, Math.max(0, scrollElement.scrollTop - D));
586
1155
  this.sharedHeaderOffset = Math.min(
587
1156
  Math.max(scrollElement.scrollTop, 0),
@@ -594,11 +1163,12 @@ export class CollapsiblePagerView extends React.PureComponent<
594
1163
  const key = this.pages().map(pageKey)[index];
595
1164
  const scrollElement = this.pageScrollElement(index);
596
1165
  if (!key || !scrollElement) return;
597
- const D = Math.max(0, this.props.headerHeight);
1166
+ const D = this.headerHeight;
598
1167
  const C = Math.min(Math.max(0, this.sharedHeaderOffset), D);
599
1168
  const expectedQ = C + (C === D ? this.pageOffsets.get(key) ?? 0 : 0);
600
1169
  this.bodyAlignment = { element: scrollElement, key };
601
1170
  scrollElement.scrollTop = expectedQ;
1171
+ this.syncFallbackHeader();
602
1172
  }
603
1173
 
604
1174
  private selectPage(selectedPage: number, animated: boolean) {
@@ -612,8 +1182,11 @@ export class CollapsiblePagerView extends React.PureComponent<
612
1182
  ) {
613
1183
  return;
614
1184
  }
1185
+ this.synchronizeScrollOwnerIdentity(this.state.selectedPage);
615
1186
  const selectionToken = ++this.selectionRestoreToken;
616
1187
  const expectedKey = this.pages().map(pageKey)[position];
1188
+ if (this.routeTarget && this.routeTarget.key !== expectedKey)
1189
+ this.setRouteTarget(null);
617
1190
  this.cancelPendingTopFrame();
618
1191
  this.savePageOffset(this.state.selectedPage);
619
1192
 
@@ -632,6 +1205,12 @@ export class CollapsiblePagerView extends React.PureComponent<
632
1205
  )
633
1206
  return;
634
1207
  this.configureActiveScrollElement();
1208
+ if (
1209
+ this.selectionRestoreToken !== selectionToken ||
1210
+ this.routeTarget?.key === expectedKey
1211
+ ) {
1212
+ return;
1213
+ }
635
1214
  this.restorePageOffset(position);
636
1215
  };
637
1216
  if (typeof requestAnimationFrame === "function") {
@@ -679,7 +1258,7 @@ export class CollapsiblePagerView extends React.PureComponent<
679
1258
  this.pageScrollElement(this.state.selectedPage)?.scrollTop ?? 0,
680
1259
  0
681
1260
  ),
682
- Math.max(0, this.props.headerHeight)
1261
+ this.headerHeight
683
1262
  ),
684
1263
  nativePageCount: pages.length,
685
1264
  attachedPageCount: retainedPages.length,
@@ -770,6 +1349,7 @@ export class CollapsiblePagerView extends React.PureComponent<
770
1349
  externalTopOverlayTag: _externalTopOverlayTag,
771
1350
  headerHeight: _headerHeight,
772
1351
  stickyHeaderHeight: _stickyHeaderHeight,
1352
+ isActive: _isActive,
773
1353
  pageRetentionDistance: _pageRetentionDistance,
774
1354
  layoutDirection: _layoutDirection,
775
1355
  scrollEnabled: _scrollEnabled,
@@ -787,18 +1367,20 @@ export class CollapsiblePagerView extends React.PureComponent<
787
1367
  const keys = pages.map(pageKey);
788
1368
  const retained = new Set(this.mountedPages(pages.length));
789
1369
  const pageWidth = pages.length > 0 ? `${100 / pages.length}%` : "100%";
790
- const collapseAnimation = {
791
- "--ok-collapsible-header-height": `${Math.max(
792
- 0,
793
- this.props.headerHeight
794
- )}px`,
795
- animationName: COLLAPSE_ANIMATION_NAME,
796
- animationDuration: "auto",
797
- animationFillMode: "both",
798
- animationTimingFunction: "linear",
799
- animationTimeline: this.timelineName,
800
- animationRange: `0px ${Math.max(1, this.props.headerHeight)}px`,
801
- } as never;
1370
+ const collapseAnimation = this.cssTimelineSupported
1371
+ ? ({
1372
+ "--ok-collapsible-header-height": `${Math.max(
1373
+ 0,
1374
+ this.headerHeight
1375
+ )}px`,
1376
+ animationName: COLLAPSE_ANIMATION_NAME,
1377
+ animationDuration: "auto",
1378
+ animationFillMode: "both",
1379
+ animationTimingFunction: "linear",
1380
+ animationTimeline: this.timelineName,
1381
+ animationRange: `0px ${Math.max(1, this.headerHeight)}px`,
1382
+ } as never)
1383
+ : undefined;
802
1384
  const trackStyle = [
803
1385
  styles.track,
804
1386
  {
@@ -818,35 +1400,45 @@ export class CollapsiblePagerView extends React.PureComponent<
818
1400
  return (
819
1401
  <View
820
1402
  {...viewProps}
821
- ref={(node) => {
822
- this.root = webElement(node);
823
- }}
1403
+ ref={this.setRoot}
824
1404
  style={[
825
1405
  styles.root,
826
- { timelineScope: this.timelineName } as never,
1406
+ this.cssTimelineSupported
1407
+ ? ({ timelineScope: this.timelineName } as never)
1408
+ : undefined,
827
1409
  style,
828
1410
  ]}
829
1411
  >
830
1412
  <View
1413
+ ref={(node) => {
1414
+ this.headerSlot = webElement(node);
1415
+ }}
831
1416
  style={[
832
1417
  styles.header,
833
- { height: Math.max(0, this.props.headerHeight) },
1418
+ { height: this.headerHeight },
834
1419
  collapseAnimation,
835
1420
  ]}
836
1421
  >
837
- {header}
1422
+ {this.hasExplicitHeaderHeight ? header : (
1423
+ <View ref={this.setHeaderContent}>{header}</View>
1424
+ )}
838
1425
  </View>
839
1426
  <View
1427
+ ref={(node) => {
1428
+ this.stickySlot = webElement(node);
1429
+ }}
840
1430
  style={[
841
1431
  styles.sticky,
842
1432
  {
843
- top: Math.max(0, this.props.headerHeight),
844
- height: Math.max(0, this.props.stickyHeaderHeight),
1433
+ top: this.headerHeight,
1434
+ height: this.stickyHeaderHeight,
845
1435
  },
846
1436
  collapseAnimation,
847
1437
  ]}
848
1438
  >
849
- {stickyHeader}
1439
+ {this.hasExplicitStickyHeaderHeight ? stickyHeader : (
1440
+ <View ref={this.setStickyContent}>{stickyHeader}</View>
1441
+ )}
850
1442
  </View>
851
1443
  <View
852
1444
  ref={this.setTrack}
@@ -871,7 +1463,21 @@ export class CollapsiblePagerView extends React.PureComponent<
871
1463
  style={[styles.page, { width: pageWidth }]}
872
1464
  aria-hidden={index !== this.state.selectedPage}
873
1465
  >
874
- {retained.has(index) ? page : null}
1466
+ {retained.has(index) ? (
1467
+ <WebCollapsiblePagerPage
1468
+ pageKey={keys[index]}
1469
+ isFocused={index === this.state.selectedPage}
1470
+ isRouteActive={this.props.isActive !== false}
1471
+ headerHeight={this.headerHeight}
1472
+ stickyHeaderHeight={this.stickyHeaderHeight}
1473
+ onBridge={this.onPageBridge}
1474
+ onGeometryChange={this.onPageGeometryChange}
1475
+ onScroll={this.onPageScroll}
1476
+ onUserInput={this.onPageUserInput}
1477
+ >
1478
+ {page}
1479
+ </WebCollapsiblePagerPage>
1480
+ ) : null}
875
1481
  </View>
876
1482
  ))}
877
1483
  </View>
@@ -883,7 +1489,7 @@ export class CollapsiblePagerView extends React.PureComponent<
883
1489
  const styles = StyleSheet.create({
884
1490
  root: {
885
1491
  flex: 1,
886
- overflow: "hidden",
1492
+ overflow: "clip",
887
1493
  position: "relative",
888
1494
  } as never,
889
1495
  header: {
@@ -899,7 +1505,7 @@ const styles = StyleSheet.create({
899
1505
  left: 0,
900
1506
  right: 0,
901
1507
  zIndex: 4,
902
- overflow: "hidden",
1508
+ overflow: "visible",
903
1509
  } as never,
904
1510
  track: {
905
1511
  position: "absolute",