@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,7 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  import React from "react";
4
- import { I18nManager, StyleSheet, View } from "react-native";
4
+ import { I18nManager, Platform, StyleSheet, View } from "react-native";
5
+ import { WebCollapsiblePagerPage } from "./WebCollapsiblePagerPage.js";
5
6
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
7
  const COLLAPSE_ANIMATION_NAME = "ok-collapsible-pager-collapse";
7
8
  const COLLAPSE_STYLE_ID = "ok-collapsible-pager-styles";
@@ -18,6 +19,11 @@ const webElement = node => {
18
19
  return element && typeof element.querySelector === "function" && typeof element.addEventListener === "function" ? element : null;
19
20
  };
20
21
  const timelineStyle = element => element.style;
22
+ function scrollElementTo(element, options) {
23
+ // RN Web decorates ScrollView nodes with a React Native scrollTo signature.
24
+ const scrollTo = element.ownerDocument.defaultView?.Element.prototype.scrollTo ?? element.scrollTo;
25
+ scrollTo.call(element, options);
26
+ }
21
27
  function ensureCollapseAnimation(document) {
22
28
  if (document.getElementById(COLLAPSE_STYLE_ID)) return;
23
29
  const style = document.createElement("style");
@@ -25,6 +31,10 @@ function ensureCollapseAnimation(document) {
25
31
  style.textContent = `@keyframes ${COLLAPSE_ANIMATION_NAME}{from{transform:translateY(0)}to{transform:translateY(calc(-1 * var(--ok-collapsible-header-height)))}}`;
26
32
  document.head.appendChild(style);
27
33
  }
34
+ function supportsScrollTimeline(document) {
35
+ const css = document.defaultView?.CSS;
36
+ return !!(typeof css?.supports === "function" && css.supports("animation-timeline", "--ok-test") && css.supports("timeline-scope", "--ok-test") && css.supports("scroll-timeline-name", "--ok-test") && css.supports("scroll-timeline-axis", "block") && css.supports("animation-range", "0px 1px") && css.supports("animation-duration", "auto"));
37
+ }
28
38
  const pageKey = (child, index) => {
29
39
  if (/*#__PURE__*/React.isValidElement(child) && child.key != null) return String(child.key);
30
40
  return `page-${index}`;
@@ -38,11 +48,30 @@ export class CollapsiblePagerView extends React.PureComponent {
38
48
  };
39
49
  root = null;
40
50
  track = null;
51
+ headerSlot = null;
52
+ stickySlot = null;
53
+ cssTimelineSupported = false;
54
+ overlayTouch = null;
41
55
  touchStartX = null;
42
56
  touchStartY = null;
43
57
  horizontalTouch = false;
44
58
  pageOffsets = new Map();
45
59
  pageHosts = new Map();
60
+ pageScrollIdentities = new Map();
61
+ pageBridges = new Map();
62
+ ownerRecords = new Map();
63
+ ownerGeneration = 0;
64
+ lastRawTops = new Map();
65
+ routeTarget = null;
66
+ measuredHeaderHeight = 0;
67
+ measuredStickyHeight = 0;
68
+ headerMeasured = false;
69
+ stickyMeasured = false;
70
+ headerContent = null;
71
+ stickyContent = null;
72
+ chromeObserver = null;
73
+ chromeMeasureFrame = null;
74
+ disposed = false;
46
75
  sharedHeaderOffset = 0;
47
76
  selectionRestoreToken = 0;
48
77
  bodyAlignment = null;
@@ -54,13 +83,51 @@ export class CollapsiblePagerView extends React.PureComponent {
54
83
  pageScrollState = "idle";
55
84
  configuredScrollElement = null;
56
85
  timelineName = `--ok-collapsible-pager-${++collapsiblePagerWebInstance}`;
86
+ get headerHeight() {
87
+ return this.hasExplicitHeaderHeight ? this.props.headerHeight ?? this.measuredHeaderHeight : this.measuredHeaderHeight;
88
+ }
89
+ get hasExplicitHeaderHeight() {
90
+ return this.props.headerHeight !== undefined && Number.isFinite(this.props.headerHeight) && this.props.headerHeight >= 0;
91
+ }
92
+ get stickyHeaderHeight() {
93
+ return this.hasExplicitStickyHeaderHeight ? this.props.stickyHeaderHeight ?? this.measuredStickyHeight : this.measuredStickyHeight;
94
+ }
95
+ get hasExplicitStickyHeaderHeight() {
96
+ return this.props.stickyHeaderHeight !== undefined && Number.isFinite(this.props.stickyHeaderHeight) && this.props.stickyHeaderHeight >= 0;
97
+ }
98
+ getSnapshotBeforeUpdate(previousProps) {
99
+ if (previousProps.isActive !== false && this.props.isActive === false) return this.captureRouteTarget(previousProps);
100
+ return null;
101
+ }
57
102
  componentDidMount() {
58
- if (this.root) ensureCollapseAnimation(this.root.ownerDocument);
103
+ this.disposed = false;
104
+ if (Platform.OS !== "web" || typeof document === "undefined") throw new Error("WebCollapsiblePagerView requires a Web DOM runtime");
105
+ if (!this.hasExplicitHeaderHeight) this.headerMeasured = false;
106
+ if (!this.hasExplicitStickyHeaderHeight) this.stickyMeasured = false;
107
+ this.observeChrome();
108
+ if (this.root) {
109
+ this.cssTimelineSupported = supportsScrollTimeline(this.root.ownerDocument);
110
+ if (this.cssTimelineSupported) {
111
+ ensureCollapseAnimation(this.root.ownerDocument);
112
+ this.forceUpdate();
113
+ }
114
+ }
59
115
  this.scheduleScrollConfiguration();
116
+ this.syncFallbackHeader();
60
117
  this.notifyMountedPagesChanged();
61
118
  this.emitDiagnostics("mounted");
62
119
  }
63
- componentDidUpdate(previousProps) {
120
+ componentDidUpdate(previousProps, _previousState, routeSnapshot) {
121
+ if (routeSnapshot) {
122
+ const record = this.ownerRecords.get(routeSnapshot.key);
123
+ if (this.pages().map(pageKey).includes(routeSnapshot.key) && record?.element === routeSnapshot.owner && record.identity === routeSnapshot.identity && record.generation === routeSnapshot.generation) this.setRouteTarget(routeSnapshot);
124
+ }
125
+ if (previousProps.isActive === false && this.props.isActive !== false) {
126
+ if (!this.hasExplicitHeaderHeight) this.headerMeasured = false;
127
+ if (!this.hasExplicitStickyHeaderHeight) this.stickyMeasured = false;
128
+ this.scheduleChromeMeasurement();
129
+ this.restoreScrollPosition();
130
+ }
64
131
  if (previousProps.scrollEnabled !== this.props.scrollEnabled) {
65
132
  this.scrollEnabled = this.props.scrollEnabled ?? true;
66
133
  if (!this.scrollEnabled) {
@@ -82,10 +149,29 @@ export class CollapsiblePagerView extends React.PureComponent {
82
149
  });
83
150
  return;
84
151
  }
152
+ // Child layout effects can run before page-host refs are reattached.
153
+ // Recheck identities in the same commit, before a new user command runs.
154
+ this.mountedPages().forEach(index => this.synchronizeScrollOwnerIdentity(index));
155
+ const currentKeys = new Set(this.pages().map(pageKey));
156
+ const ownedKeys = new Set([...this.pageScrollIdentities.keys(), ...this.pageOffsets.keys(), ...this.lastRawTops.keys(), ...this.ownerRecords.keys()]);
157
+ for (const key of ownedKeys) {
158
+ if (!currentKeys.has(key)) {
159
+ this.pageScrollIdentities.delete(key);
160
+ this.pageOffsets.delete(key);
161
+ this.lastRawTops.delete(key);
162
+ this.ownerRecords.delete(key);
163
+ if (this.routeTarget?.key === key) this.setRouteTarget(null);
164
+ }
165
+ }
85
166
  this.scheduleScrollConfiguration();
167
+ this.syncFallbackHeader();
86
168
  this.notifyMountedPagesChanged();
87
169
  }
88
170
  componentWillUnmount() {
171
+ // Activity can disconnect effects before the inactive update commits.
172
+ const routeTarget = this.captureRouteTarget(this.props);
173
+ if (routeTarget) this.setRouteTarget(routeTarget);
174
+ this.disposed = true;
89
175
  this.selectionRestoreToken += 1;
90
176
  this.cancelPendingTopFrame();
91
177
  this.savePageOffset(this.state.selectedPage);
@@ -98,8 +184,148 @@ export class CollapsiblePagerView extends React.PureComponent {
98
184
  this.transitionTimer = null;
99
185
  }
100
186
  this.setTrack(null);
187
+ this.setRoot(null);
188
+ this.chromeObserver?.disconnect();
189
+ this.chromeObserver = null;
190
+ if (this.chromeMeasureFrame !== null) {
191
+ cancelAnimationFrame(this.chromeMeasureFrame);
192
+ this.chromeMeasureFrame = null;
193
+ }
194
+ this.pageBridges.clear();
101
195
  this.restoreConfiguredScrollElement();
102
196
  }
197
+ setHeaderContent = node => {
198
+ const element = webElement(node);
199
+ if (element === this.headerContent) return;
200
+ if (this.headerContent) this.chromeObserver?.unobserve(this.headerContent);
201
+ this.headerContent = element;
202
+ this.headerMeasured = false;
203
+ if (element) this.chromeObserver?.observe(element);
204
+ this.scheduleChromeMeasurement();
205
+ };
206
+ setStickyContent = node => {
207
+ const element = webElement(node);
208
+ if (element === this.stickyContent) return;
209
+ if (this.stickyContent) this.chromeObserver?.unobserve(this.stickyContent);
210
+ this.stickyContent = element;
211
+ this.stickyMeasured = false;
212
+ if (element) this.chromeObserver?.observe(element);
213
+ this.scheduleChromeMeasurement();
214
+ };
215
+ observeChrome() {
216
+ this.chromeObserver = new ResizeObserver(this.scheduleChromeMeasurement);
217
+ if (this.headerContent) this.chromeObserver.observe(this.headerContent);
218
+ if (this.stickyContent) this.chromeObserver.observe(this.stickyContent);
219
+ this.scheduleChromeMeasurement();
220
+ }
221
+ scheduleChromeMeasurement = () => {
222
+ if (this.disposed || this.chromeMeasureFrame !== null || typeof requestAnimationFrame !== "function") return;
223
+ this.chromeMeasureFrame = requestAnimationFrame(() => {
224
+ this.chromeMeasureFrame = null;
225
+ const header = this.headerContent;
226
+ const sticky = this.stickyContent;
227
+ const nextHeader = header?.isConnected ? Math.max(0, header.getBoundingClientRect().height) : 0;
228
+ const nextSticky = sticky?.isConnected ? Math.max(0, sticky.getBoundingClientRect().height) : 0;
229
+ const headerChanged = this.measuredHeaderHeight !== nextHeader || this.headerMeasured !== Boolean(header?.isConnected);
230
+ const stickyChanged = this.measuredStickyHeight !== nextSticky || this.stickyMeasured !== Boolean(sticky?.isConnected);
231
+ this.measuredHeaderHeight = nextHeader;
232
+ this.measuredStickyHeight = nextSticky;
233
+ this.headerMeasured = Boolean(header?.isConnected);
234
+ this.stickyMeasured = Boolean(sticky?.isConnected);
235
+ if (headerChanged || stickyChanged) {
236
+ this.forceUpdate();
237
+ this.scheduleScrollConfiguration();
238
+ }
239
+ });
240
+ };
241
+ onPageBridge = (key, bridge) => {
242
+ if (bridge) {
243
+ this.pageBridges.set(key, bridge);
244
+ bridge.setPendingRouteScrollTop(this.routeTarget?.key === key ? this.routeTarget.top : null);
245
+ const index = this.pages().map(pageKey).indexOf(key);
246
+ if (index >= 0) this.syncPageOwner(index);
247
+ } else {
248
+ this.pageBridges.delete(key);
249
+ }
250
+ };
251
+ setRouteTarget(target) {
252
+ const oldKey = this.routeTarget?.key;
253
+ this.routeTarget = target;
254
+ if (oldKey) this.pageBridges.get(oldKey)?.setPendingRouteScrollTop(null);
255
+ if (target) this.pageBridges.get(target.key)?.setPendingRouteScrollTop(target.top);
256
+ }
257
+ syncPageOwner(index) {
258
+ const key = this.pages().map(pageKey)[index];
259
+ const element = this.pageScrollElement(index);
260
+ if (!key || !element) return;
261
+ const identity = element.getAttribute("data-collapsible-pager-scroll-key") ?? key;
262
+ const previous = this.ownerRecords.get(key);
263
+ if (previous?.element !== element || previous.identity !== identity) {
264
+ if (this.routeTarget?.key === key) this.setRouteTarget(null);
265
+ this.ownerRecords.set(key, {
266
+ element,
267
+ identity,
268
+ generation: ++this.ownerGeneration
269
+ });
270
+ this.lastRawTops.delete(key);
271
+ }
272
+ this.pageBridges.get(key)?.setOwner(element);
273
+ }
274
+ captureRouteTarget(previousProps) {
275
+ const index = this.state.selectedPage;
276
+ const oldKey = React.Children.toArray(previousProps.children).map(pageKey)[index];
277
+ const key = this.configuredScrollElement?.key ?? oldKey;
278
+ const record = key ? this.ownerRecords.get(key) : null;
279
+ if (!record?.element.isConnected) return null;
280
+ if (this.routeTarget?.key === key && this.routeTarget.identity === record.identity && this.routeTarget.owner === record.element && this.routeTarget.generation === record.generation) return this.routeTarget;
281
+ const top = record.element.clientHeight > 0 ? record.element.scrollTop : this.lastRawTops.get(key) ?? record.element.scrollTop;
282
+ return {
283
+ key,
284
+ identity: record.identity,
285
+ owner: record.element,
286
+ generation: record.generation,
287
+ top: Math.max(0, top)
288
+ };
289
+ }
290
+ restoreScrollPosition = () => {
291
+ if (this.disposed || this.props.isActive === false) return false;
292
+ const pending = this.routeTarget;
293
+ if (!pending) return true;
294
+ const key = this.pages().map(pageKey)[this.state.selectedPage];
295
+ if (key !== pending.key) return false;
296
+ this.syncPageOwner(this.state.selectedPage);
297
+ const record = this.ownerRecords.get(key);
298
+ if (!record || record.element !== pending.owner || record.identity !== pending.identity || record.generation !== pending.generation) return false;
299
+ const element = record.element;
300
+ const configured = this.configuredScrollElement;
301
+ if (!element.isConnected || element.clientHeight <= 0 || configured?.element !== element || configured.key !== key || !this.hasExplicitHeaderHeight && !this.headerMeasured || !this.hasExplicitStickyHeaderHeight && !this.stickyMeasured || element.getAttribute("data-collapsible-pager-active") !== "true" || element.style.getPropertyValue(PAGER_HEADER_INSET).trim() !== `${this.headerHeight}px` || element.style.getPropertyValue(PAGER_STICKY_INSET).trim() !== `${this.stickyHeaderHeight}px` || element.scrollHeight - element.clientHeight < pending.top) return false;
302
+ this.selectionRestoreToken += 1;
303
+ this.cancelPendingTopFrame();
304
+ this.setRouteTarget(null);
305
+ this.lastRawTops.set(key, pending.top);
306
+ const D = this.headerHeight;
307
+ this.sharedHeaderOffset = Math.min(pending.top, D);
308
+ this.pageOffsets.set(key, Math.max(0, pending.top - D));
309
+ this.bodyAlignment = {
310
+ element,
311
+ key
312
+ };
313
+ scrollElementTo(element, {
314
+ top: pending.top,
315
+ behavior: "instant"
316
+ });
317
+ this.syncFallbackHeader();
318
+ return true;
319
+ };
320
+ onPageScroll = (key, top) => {
321
+ if (this.props.isActive !== false) this.lastRawTops.set(key, top);
322
+ };
323
+ onPageGeometryChange = key => {
324
+ if (this.routeTarget?.key === key) this.restoreScrollPosition();
325
+ };
326
+ onPageUserInput = key => {
327
+ if (this.routeTarget?.key === key) this.setRouteTarget(null);
328
+ };
103
329
  setPage = selectedPage => {
104
330
  this.selectPage(selectedPage, true);
105
331
  };
@@ -109,8 +335,17 @@ export class CollapsiblePagerView extends React.PureComponent {
109
335
  scrollToPageTop = options => {
110
336
  this.scrollCurrentPageToTop(0, options?.animated ?? true);
111
337
  };
338
+ scrollHeaderBy = delta => {
339
+ if (!Number.isFinite(delta)) return;
340
+ const offset = Math.min(this.headerHeight, Math.max(0, this.sharedHeaderOffset + delta));
341
+ this.scrollCurrentPageToTop(offset, false);
342
+ };
343
+ scrollHeaderToOffset = offset => {
344
+ if (!Number.isFinite(offset)) return;
345
+ this.scrollCurrentPageToTop(Math.min(this.headerHeight, Math.max(0, offset)), false);
346
+ };
112
347
  scrollToStickyTop = options => {
113
- this.scrollCurrentPageToTop(Math.max(0, this.props.headerHeight), options?.animated ?? true);
348
+ this.scrollCurrentPageToTop(this.headerHeight, options?.animated ?? true);
114
349
  };
115
350
  cancelPendingTopFrame = () => {
116
351
  if (!this.pendingTopFrame) return;
@@ -145,11 +380,12 @@ export class CollapsiblePagerView extends React.PureComponent {
145
380
  const element = this.pageScrollElement(this.state.selectedPage);
146
381
  const key = this.pages().map(pageKey)[this.state.selectedPage];
147
382
  if (!element?.isConnected || !key) return;
383
+ this.setRouteTarget(null);
148
384
  this.selectionRestoreToken += 1;
149
385
  this.cancelPendingTopFrame();
150
386
  const EventConstructor = element.ownerDocument.defaultView?.Event;
151
387
  if (EventConstructor) element.dispatchEvent(new EventConstructor("ok-collapsible-pager-cancel-scroll-command"));
152
- element.scrollTo({
388
+ scrollElementTo(element, {
153
389
  left: element.scrollLeft,
154
390
  top: element.scrollTop,
155
391
  behavior: "instant"
@@ -159,15 +395,16 @@ export class CollapsiblePagerView extends React.PureComponent {
159
395
  this.pendingTopFrame = null;
160
396
  if (!element.isConnected || this.horizontalTouch || this.transitionTimer !== null || this.pageScrollElement(this.state.selectedPage) !== element || this.pages().map(pageKey)[this.state.selectedPage] !== key) return;
161
397
  this.pageOffsets.set(key, 0);
162
- this.sharedHeaderOffset = offset;
398
+ this.sharedHeaderOffset = Math.min(this.headerHeight, Math.max(0, offset));
163
399
  this.bodyAlignment = {
164
400
  element,
165
401
  key
166
402
  };
167
- element.scrollTo({
168
- top: offset,
403
+ scrollElementTo(element, {
404
+ top: this.sharedHeaderOffset,
169
405
  behavior: animated ? "smooth" : "instant"
170
406
  });
407
+ this.syncFallbackHeader();
171
408
  });
172
409
  this.pendingTopFrame = {
173
410
  frame,
@@ -230,8 +467,119 @@ export class CollapsiblePagerView extends React.PureComponent {
230
467
  };
231
468
  pageScrollElement(index) {
232
469
  const pageHost = this.pageHosts.get(index);
233
- return pageHost?.querySelector(".ok-native-list-viewport, [data-collapsible-pager-scroll]") ?? pageHost;
470
+ return pageHost?.querySelector("[data-collapsible-pager-scroll]") ?? pageHost?.querySelector(".ok-native-list-viewport") ?? pageHost;
471
+ }
472
+ setRoot = node => {
473
+ const root = webElement(node);
474
+ if (root === this.root) return;
475
+ this.root?.removeEventListener("ok-collapsible-pager-scroll-key-changed", this.onScrollOwnerIdentityChanged);
476
+ this.root?.removeEventListener("wheel", this.onOverlayWheel);
477
+ this.root?.removeEventListener("touchstart", this.onOverlayTouchStart);
478
+ this.root?.removeEventListener("touchmove", this.onOverlayTouchMove);
479
+ this.root?.removeEventListener("touchend", this.onOverlayTouchEnd);
480
+ this.root?.removeEventListener("touchcancel", this.onOverlayTouchEnd);
481
+ this.overlayTouch = null;
482
+ this.root = root;
483
+ root?.addEventListener("ok-collapsible-pager-scroll-key-changed", this.onScrollOwnerIdentityChanged);
484
+ root?.addEventListener("wheel", this.onOverlayWheel, {
485
+ passive: false
486
+ });
487
+ root?.addEventListener("touchstart", this.onOverlayTouchStart, {
488
+ passive: true
489
+ });
490
+ root?.addEventListener("touchmove", this.onOverlayTouchMove, {
491
+ passive: false
492
+ });
493
+ root?.addEventListener("touchend", this.onOverlayTouchEnd);
494
+ root?.addEventListener("touchcancel", this.onOverlayTouchEnd);
495
+ };
496
+ overlayTarget(event) {
497
+ const target = event.target;
498
+ if (!target || !this.headerSlot?.contains(target) && !this.stickySlot?.contains(target) || target.closest("input,textarea,select,[contenteditable=true]")) return null;
499
+ return target;
500
+ }
501
+ nestedScrollConsumes(target, delta) {
502
+ for (let node = target; node && node !== this.root; node = node.parentElement) {
503
+ const style = node.ownerDocument.defaultView?.getComputedStyle(node);
504
+ if (/auto|scroll/.test(style?.overflowY ?? "") && node.scrollHeight > node.clientHeight && (delta < 0 ? node.scrollTop > 0 : node.scrollTop < node.scrollHeight - node.clientHeight)) return true;
505
+ }
506
+ return false;
234
507
  }
508
+ scrollOverlayOwner(owner, delta, event) {
509
+ if (!event.cancelable) return;
510
+ const EventConstructor = owner.ownerDocument.defaultView?.Event;
511
+ if (EventConstructor) owner.dispatchEvent(new EventConstructor("ok-collapsible-pager-user-scroll"));
512
+ const top = Math.min(Math.max(0, owner.scrollTop + delta), owner.scrollHeight - owner.clientHeight);
513
+ if (top === owner.scrollTop) return;
514
+ this.takeOverVerticalScroll();
515
+ scrollElementTo(owner, {
516
+ top,
517
+ behavior: "instant"
518
+ });
519
+ event.preventDefault();
520
+ }
521
+ onOverlayWheel = event => {
522
+ const target = this.overlayTarget(event);
523
+ const owner = this.pageScrollElement(this.state.selectedPage);
524
+ if (!target || !owner || event.defaultPrevented || event.ctrlKey || Math.abs(event.deltaX) > Math.abs(event.deltaY)) return;
525
+ const lineHeight = Number.parseFloat(target.ownerDocument.defaultView?.getComputedStyle(target).lineHeight ?? "") || 16;
526
+ const delta = event.deltaY * (event.deltaMode === 1 ? lineHeight : event.deltaMode === 2 ? owner.clientHeight : 1);
527
+ if (!this.nestedScrollConsumes(target, delta)) this.scrollOverlayOwner(owner, delta, event);
528
+ };
529
+ onOverlayTouchStart = event => {
530
+ const target = this.overlayTarget(event);
531
+ const owner = this.pageScrollElement(this.state.selectedPage);
532
+ const touch = event.touches.length === 1 ? event.touches[0] : null;
533
+ this.overlayTouch = target && owner && touch ? {
534
+ owner,
535
+ target,
536
+ x: touch.clientX,
537
+ y: touch.clientY,
538
+ lastY: touch.clientY
539
+ } : null;
540
+ };
541
+ onOverlayTouchMove = event => {
542
+ if (event.defaultPrevented) return;
543
+ const pending = this.overlayTouch;
544
+ const touch = event.touches.length === 1 ? event.touches[0] : null;
545
+ if (!pending || !touch || pending.owner !== this.pageScrollElement(this.state.selectedPage)) {
546
+ this.overlayTouch = null;
547
+ return;
548
+ }
549
+ const delta = pending.lastY - touch.clientY;
550
+ pending.lastY = touch.clientY;
551
+ if (Math.abs(touch.clientY - pending.y) < 8 || Math.abs(touch.clientX - pending.x) >= Math.abs(touch.clientY - pending.y)) return;
552
+ if (!this.nestedScrollConsumes(pending.target, delta)) this.scrollOverlayOwner(pending.owner, delta, event);
553
+ };
554
+ onOverlayTouchEnd = () => {
555
+ this.overlayTouch = null;
556
+ };
557
+ synchronizeScrollOwnerIdentity(index) {
558
+ const key = this.pages().map(pageKey)[index];
559
+ const element = this.pageScrollElement(index);
560
+ if (!key || !element) return;
561
+ const identity = element.getAttribute("data-collapsible-pager-scroll-key") ?? key;
562
+ const previous = this.pageScrollIdentities.get(key);
563
+ this.pageScrollIdentities.set(key, identity);
564
+ this.syncPageOwner(index);
565
+ if (previous === undefined || previous === identity) return;
566
+ if (index === this.state.selectedPage) {
567
+ this.selectionRestoreToken += 1;
568
+ this.cancelPendingTopFrame();
569
+ this.sharedHeaderOffset = 0;
570
+ this.bodyAlignment = null;
571
+ scrollElementTo(element, {
572
+ top: 0,
573
+ behavior: "instant"
574
+ });
575
+ }
576
+ this.pageOffsets.delete(key);
577
+ }
578
+ onScrollOwnerIdentityChanged = event => {
579
+ this.pages().forEach((_, index) => {
580
+ if (event.target === this.pageScrollElement(index)) this.synchronizeScrollOwnerIdentity(index);
581
+ });
582
+ };
235
583
  notifyInsetsChanged(element) {
236
584
  const EventConstructor = element.ownerDocument.defaultView?.Event;
237
585
  if (EventConstructor) {
@@ -247,10 +595,11 @@ export class CollapsiblePagerView extends React.PureComponent {
247
595
  configured.element.removeEventListener("pointerdown", this.onScrollbarPointerDown);
248
596
  configured.element.removeEventListener("keydown", this.onQueuedTopKey);
249
597
  configured.element.removeEventListener("scrollend", this.onVerticalScrollSettled);
598
+ configured.element.removeEventListener("scroll", this.syncFallbackHeader);
250
599
  const style = timelineStyle(configured.element);
251
600
  configured.rangeSpacer.remove();
252
601
  style.position = configured.position;
253
- style.paddingTop = configured.paddingTop;
602
+ configured.paddingTarget.style.paddingTop = configured.paddingTop;
254
603
  style.boxSizing = configured.boxSizing;
255
604
  style.scrollTimelineName = configured.scrollTimelineName;
256
605
  style.scrollTimelineAxis = configured.scrollTimelineAxis;
@@ -259,16 +608,19 @@ export class CollapsiblePagerView extends React.PureComponent {
259
608
  configured.element.removeAttribute("data-collapsible-pager-active");
260
609
  this.notifyInsetsChanged(configured.element);
261
610
  this.configuredScrollElement = null;
611
+ this.syncFallbackHeader();
262
612
  if (this.bodyAlignment?.element === configured.element) this.bodyAlignment = null;
263
613
  }
264
614
  configureActiveScrollElement = () => {
265
615
  this.configureFrame = null;
266
616
  const element = this.pageScrollElement(this.state.selectedPage);
267
617
  if (!element) return;
618
+ this.synchronizeScrollOwnerIdentity(this.state.selectedPage);
268
619
  if (this.configuredScrollElement?.element !== element) {
269
620
  this.restoreConfiguredScrollElement();
270
621
  const style = timelineStyle(element);
271
- const computedPaddingTop = element.ownerDocument.defaultView?.getComputedStyle(element).paddingTop || style.paddingTop || "0px";
622
+ const paddingTarget = element.hasAttribute("data-collapsible-pager-scroll") ? webElement(element.firstElementChild) ?? element : element;
623
+ const computedPaddingTop = element.ownerDocument.defaultView?.getComputedStyle(paddingTarget).paddingTop || paddingTarget.style.paddingTop || "0px";
272
624
  const rangeSpacer = element.ownerDocument.createElement("div");
273
625
  Object.assign(rangeSpacer.style, {
274
626
  position: "absolute",
@@ -283,10 +635,12 @@ export class CollapsiblePagerView extends React.PureComponent {
283
635
  if (element.ownerDocument.defaultView?.getComputedStyle(element).position === "static") style.position = "relative";
284
636
  element.appendChild(rangeSpacer);
285
637
  this.configuredScrollElement = {
638
+ key: this.pages().map(pageKey)[this.state.selectedPage],
286
639
  element,
640
+ paddingTarget,
287
641
  rangeSpacer,
288
642
  position,
289
- paddingTop: style.paddingTop,
643
+ paddingTop: paddingTarget.style.paddingTop,
290
644
  basePaddingTop: computedPaddingTop,
291
645
  boxSizing: style.boxSizing,
292
646
  scrollTimelineName: style.scrollTimelineName,
@@ -299,17 +653,21 @@ export class CollapsiblePagerView extends React.PureComponent {
299
653
  element.addEventListener("wheel", this.onQueuedTopWheel);
300
654
  element.addEventListener("pointerdown", this.onScrollbarPointerDown);
301
655
  element.addEventListener("keydown", this.onQueuedTopKey);
656
+ if (!this.cssTimelineSupported) element.addEventListener("scroll", this.syncFallbackHeader, {
657
+ passive: true
658
+ });
302
659
  }
303
660
  const style = timelineStyle(element);
304
- const headerInset = Math.max(0, this.props.headerHeight);
305
- const stickyInset = Math.max(0, this.props.stickyHeaderHeight);
661
+ const paddingStyle = this.configuredScrollElement?.paddingTarget.style ?? style;
662
+ const headerInset = this.headerHeight;
663
+ const stickyInset = this.stickyHeaderHeight;
306
664
  if (this.configuredScrollElement) this.configuredScrollElement.rangeSpacer.style.height = `calc(100% + ${headerInset}px)`;
307
665
  const basePaddingTop = this.configuredScrollElement?.basePaddingTop ?? "0px";
308
666
  const paddingTop = `calc(${basePaddingTop} + ${headerInset + stickyInset}px)`;
309
667
  const headerInsetValue = `${headerInset}px`;
310
668
  const stickyInsetValue = `${stickyInset}px`;
311
- const insetsChanged = style.paddingTop !== paddingTop || style.getPropertyValue(PAGER_HEADER_INSET) !== headerInsetValue || style.getPropertyValue(PAGER_STICKY_INSET) !== stickyInsetValue;
312
- style.paddingTop = paddingTop;
669
+ const insetsChanged = paddingStyle.paddingTop !== paddingTop || style.getPropertyValue(PAGER_HEADER_INSET) !== headerInsetValue || style.getPropertyValue(PAGER_STICKY_INSET) !== stickyInsetValue;
670
+ paddingStyle.paddingTop = paddingTop;
313
671
  style.boxSizing = "border-box";
314
672
  style.scrollTimelineName = this.timelineName;
315
673
  style.scrollTimelineAxis = "block";
@@ -317,9 +675,20 @@ export class CollapsiblePagerView extends React.PureComponent {
317
675
  style.setProperty(PAGER_STICKY_INSET, stickyInsetValue);
318
676
  element.setAttribute("data-collapsible-pager-active", "true");
319
677
  if (insetsChanged) this.notifyInsetsChanged(element);
678
+ this.syncFallbackHeader();
679
+ this.pageBridges.get(this.pages().map(pageKey)[this.state.selectedPage])?.requestMeasure();
680
+ this.restoreScrollPosition();
681
+ };
682
+ syncFallbackHeader = () => {
683
+ if (this.cssTimelineSupported) return;
684
+ const top = this.configuredScrollElement?.element.scrollTop ?? 0;
685
+ const collapse = Math.min(Math.max(top, 0), this.headerHeight);
686
+ const transform = `translateY(${-collapse}px)`;
687
+ if (this.headerSlot) this.headerSlot.style.transform = transform;
688
+ if (this.stickySlot) this.stickySlot.style.transform = transform;
320
689
  };
321
690
  scheduleScrollConfiguration() {
322
- if (this.configureFrame !== null) return;
691
+ if (this.disposed || this.configureFrame !== null) return;
323
692
  if (typeof requestAnimationFrame === "function") {
324
693
  this.configureFrame = requestAnimationFrame(this.configureActiveScrollElement);
325
694
  } else {
@@ -327,6 +696,7 @@ export class CollapsiblePagerView extends React.PureComponent {
327
696
  }
328
697
  }
329
698
  takeOverVerticalScroll = () => {
699
+ this.setRouteTarget(null);
330
700
  this.selectionRestoreToken += 1;
331
701
  this.bodyAlignment = null;
332
702
  this.cancelPendingTopFrame();
@@ -342,11 +712,13 @@ export class CollapsiblePagerView extends React.PureComponent {
342
712
  this.emitDiagnostics("scroll-settled");
343
713
  };
344
714
  savePageOffset(index) {
345
- const key = this.pages().map(pageKey)[index];
346
- const scrollElement = this.pageScrollElement(index);
715
+ const configured = index === this.state.selectedPage ? this.configuredScrollElement : null;
716
+ const key = configured?.key ?? this.pages().map(pageKey)[index];
717
+ const scrollElement = configured?.element ?? this.pageScrollElement(index);
718
+ if (configured && !scrollElement?.isConnected) return;
347
719
  if (key && scrollElement) {
348
720
  if (this.bodyAlignment?.key === key && this.bodyAlignment.element === scrollElement) return;
349
- const D = Math.max(0, this.props.headerHeight);
721
+ const D = this.headerHeight;
350
722
  this.pageOffsets.set(key, Math.max(0, scrollElement.scrollTop - D));
351
723
  this.sharedHeaderOffset = Math.min(Math.max(scrollElement.scrollTop, 0), D);
352
724
  }
@@ -355,7 +727,7 @@ export class CollapsiblePagerView extends React.PureComponent {
355
727
  const key = this.pages().map(pageKey)[index];
356
728
  const scrollElement = this.pageScrollElement(index);
357
729
  if (!key || !scrollElement) return;
358
- const D = Math.max(0, this.props.headerHeight);
730
+ const D = this.headerHeight;
359
731
  const C = Math.min(Math.max(0, this.sharedHeaderOffset), D);
360
732
  const expectedQ = C + (C === D ? this.pageOffsets.get(key) ?? 0 : 0);
361
733
  this.bodyAlignment = {
@@ -363,6 +735,7 @@ export class CollapsiblePagerView extends React.PureComponent {
363
735
  key
364
736
  };
365
737
  scrollElement.scrollTop = expectedQ;
738
+ this.syncFallbackHeader();
366
739
  }
367
740
  selectPage(selectedPage, animated) {
368
741
  const pages = this.pages();
@@ -370,8 +743,10 @@ export class CollapsiblePagerView extends React.PureComponent {
370
743
  if (position === null || position < 0 || position >= pages.length || position === this.state.selectedPage) {
371
744
  return;
372
745
  }
746
+ this.synchronizeScrollOwnerIdentity(this.state.selectedPage);
373
747
  const selectionToken = ++this.selectionRestoreToken;
374
748
  const expectedKey = this.pages().map(pageKey)[position];
749
+ if (this.routeTarget && this.routeTarget.key !== expectedKey) this.setRouteTarget(null);
375
750
  this.cancelPendingTopFrame();
376
751
  this.savePageOffset(this.state.selectedPage);
377
752
  if (animated) this.beginPageTransition();else this.finishPageTransition();
@@ -383,6 +758,9 @@ export class CollapsiblePagerView extends React.PureComponent {
383
758
  const restore = () => {
384
759
  if (this.selectionRestoreToken !== selectionToken || this.state.selectedPage !== position || this.pages().map(pageKey)[position] !== expectedKey || !this.pageScrollElement(position)?.isConnected) return;
385
760
  this.configureActiveScrollElement();
761
+ if (this.selectionRestoreToken !== selectionToken || this.routeTarget?.key === expectedKey) {
762
+ return;
763
+ }
386
764
  this.restorePageOffset(position);
387
765
  };
388
766
  if (typeof requestAnimationFrame === "function") {
@@ -419,7 +797,7 @@ export class CollapsiblePagerView extends React.PureComponent {
419
797
  const retainedPages = this.mountedPages(pages.length);
420
798
  this.props.onCollapsibleStateChanged?.(this.nativeEvent({
421
799
  position: this.state.selectedPage,
422
- headerOffset: Math.min(Math.max(this.pageScrollElement(this.state.selectedPage)?.scrollTop ?? 0, 0), Math.max(0, this.props.headerHeight)),
800
+ headerOffset: Math.min(Math.max(this.pageScrollElement(this.state.selectedPage)?.scrollTop ?? 0, 0), this.headerHeight),
423
801
  nativePageCount: pages.length,
424
802
  attachedPageCount: retainedPages.length,
425
803
  observedScrollableCount: retainedPages.reduce((count, index) => count + (this.pageScrollElement(index) ? 1 : 0), 0),
@@ -489,6 +867,7 @@ export class CollapsiblePagerView extends React.PureComponent {
489
867
  externalTopOverlayTag: _externalTopOverlayTag,
490
868
  headerHeight: _headerHeight,
491
869
  stickyHeaderHeight: _stickyHeaderHeight,
870
+ isActive: _isActive,
492
871
  pageRetentionDistance: _pageRetentionDistance,
493
872
  layoutDirection: _layoutDirection,
494
873
  scrollEnabled: _scrollEnabled,
@@ -506,15 +885,15 @@ export class CollapsiblePagerView extends React.PureComponent {
506
885
  const keys = pages.map(pageKey);
507
886
  const retained = new Set(this.mountedPages(pages.length));
508
887
  const pageWidth = pages.length > 0 ? `${100 / pages.length}%` : "100%";
509
- const collapseAnimation = {
510
- "--ok-collapsible-header-height": `${Math.max(0, this.props.headerHeight)}px`,
888
+ const collapseAnimation = this.cssTimelineSupported ? {
889
+ "--ok-collapsible-header-height": `${Math.max(0, this.headerHeight)}px`,
511
890
  animationName: COLLAPSE_ANIMATION_NAME,
512
891
  animationDuration: "auto",
513
892
  animationFillMode: "both",
514
893
  animationTimingFunction: "linear",
515
894
  animationTimeline: this.timelineName,
516
- animationRange: `0px ${Math.max(1, this.props.headerHeight)}px`
517
- };
895
+ animationRange: `0px ${Math.max(1, this.headerHeight)}px`
896
+ } : undefined;
518
897
  const trackStyle = [styles.track, {
519
898
  width: `${Math.max(1, pages.length) * 100}%`,
520
899
  transform: [{
@@ -525,23 +904,33 @@ export class CollapsiblePagerView extends React.PureComponent {
525
904
  }];
526
905
  return /*#__PURE__*/_jsxs(View, {
527
906
  ...viewProps,
528
- ref: node => {
529
- this.root = webElement(node);
530
- },
531
- style: [styles.root, {
907
+ ref: this.setRoot,
908
+ style: [styles.root, this.cssTimelineSupported ? {
532
909
  timelineScope: this.timelineName
533
- }, style],
910
+ } : undefined, style],
534
911
  children: [/*#__PURE__*/_jsx(View, {
912
+ ref: node => {
913
+ this.headerSlot = webElement(node);
914
+ },
535
915
  style: [styles.header, {
536
- height: Math.max(0, this.props.headerHeight)
916
+ height: this.headerHeight
537
917
  }, collapseAnimation],
538
- children: header
918
+ children: this.hasExplicitHeaderHeight ? header : /*#__PURE__*/_jsx(View, {
919
+ ref: this.setHeaderContent,
920
+ children: header
921
+ })
539
922
  }), /*#__PURE__*/_jsx(View, {
923
+ ref: node => {
924
+ this.stickySlot = webElement(node);
925
+ },
540
926
  style: [styles.sticky, {
541
- top: Math.max(0, this.props.headerHeight),
542
- height: Math.max(0, this.props.stickyHeaderHeight)
927
+ top: this.headerHeight,
928
+ height: this.stickyHeaderHeight
543
929
  }, collapseAnimation],
544
- children: stickyHeader
930
+ children: this.hasExplicitStickyHeaderHeight ? stickyHeader : /*#__PURE__*/_jsx(View, {
931
+ ref: this.setStickyContent,
932
+ children: stickyHeader
933
+ })
545
934
  }), /*#__PURE__*/_jsx(View, {
546
935
  ref: this.setTrack,
547
936
  style: trackStyle,
@@ -563,7 +952,18 @@ export class CollapsiblePagerView extends React.PureComponent {
563
952
  width: pageWidth
564
953
  }],
565
954
  "aria-hidden": index !== this.state.selectedPage,
566
- children: retained.has(index) ? page : null
955
+ children: retained.has(index) ? /*#__PURE__*/_jsx(WebCollapsiblePagerPage, {
956
+ pageKey: keys[index],
957
+ isFocused: index === this.state.selectedPage,
958
+ isRouteActive: this.props.isActive !== false,
959
+ headerHeight: this.headerHeight,
960
+ stickyHeaderHeight: this.stickyHeaderHeight,
961
+ onBridge: this.onPageBridge,
962
+ onGeometryChange: this.onPageGeometryChange,
963
+ onScroll: this.onPageScroll,
964
+ onUserInput: this.onPageUserInput,
965
+ children: page
966
+ }) : null
567
967
  }, keys[index]))
568
968
  })]
569
969
  });
@@ -572,7 +972,7 @@ export class CollapsiblePagerView extends React.PureComponent {
572
972
  const styles = StyleSheet.create({
573
973
  root: {
574
974
  flex: 1,
575
- overflow: "hidden",
975
+ overflow: "clip",
576
976
  position: "relative"
577
977
  },
578
978
  header: {
@@ -588,7 +988,7 @@ const styles = StyleSheet.create({
588
988
  left: 0,
589
989
  right: 0,
590
990
  zIndex: 4,
591
- overflow: "hidden"
991
+ overflow: "visible"
592
992
  },
593
993
  track: {
594
994
  position: "absolute",