@onekeyfe/react-native-pager-view 6.7.0-2 → 6.7.0-21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/android/src/androidTest/AndroidManifest.xml +7 -0
- package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +306 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +130 -30
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +72 -7
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +27 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +19 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +50 -6
- package/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt +8 -2
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +40 -0
- package/docs/SPEC.md +257 -10
- package/ios/RNCCollapsiblePagerViewComponentView.mm +614 -42
- package/ios/RNCNativeScrollerComponentView.h +2 -0
- package/ios/RNCNativeScrollerComponentView.mm +42 -0
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +403 -1
- package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +338 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +74 -9
- package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +50 -0
- package/lib/module/CollapsiblePagerView.js +32 -7
- package/lib/module/CollapsiblePagerView.web.js +439 -39
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +13 -0
- package/lib/module/NativeScroller.web.js +16 -3
- package/lib/module/WebCollapsiblePagerPage.js +182 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.native.js +9 -0
- package/lib/module/index.web.js +2 -0
- package/lib/typescript/src/CollapsiblePagerView.d.ts +10 -3
- package/lib/typescript/src/CollapsiblePagerView.web.d.ts +66 -3
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +5 -0
- package/lib/typescript/src/WebCollapsiblePagerPage.d.ts +42 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.native.d.ts +13 -0
- package/lib/typescript/src/index.web.d.ts +4 -0
- package/package.json +6 -4
- package/react-native-pager-view.podspec +1 -0
- package/src/CollapsiblePagerView.tsx +99 -8
- package/src/CollapsiblePagerView.web.tsx +657 -51
- package/src/CollapsiblePagerViewNativeComponent.ts +13 -0
- package/src/NativeScroller.web.tsx +24 -2
- package/src/WebCollapsiblePagerPage.tsx +258 -0
- package/src/index.native.tsx +30 -0
- package/src/index.tsx +4 -0
- 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
|
|
69
|
-
stickyHeaderHeight
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
338
|
-
top:
|
|
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
|
-
|
|
416
|
-
|
|
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(
|
|
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
|
|
516
|
-
|
|
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
|
-
|
|
1077
|
+
paddingStyle.paddingTop !== paddingTop ||
|
|
528
1078
|
style.getPropertyValue(PAGER_HEADER_INSET) !== headerInsetValue ||
|
|
529
1079
|
style.getPropertyValue(PAGER_STICKY_INSET) !== stickyInsetValue;
|
|
530
|
-
|
|
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
|
|
577
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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={
|
|
822
|
-
this.root = webElement(node);
|
|
823
|
-
}}
|
|
1403
|
+
ref={this.setRoot}
|
|
824
1404
|
style={[
|
|
825
1405
|
styles.root,
|
|
826
|
-
|
|
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:
|
|
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:
|
|
844
|
-
height:
|
|
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) ?
|
|
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: "
|
|
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: "
|
|
1508
|
+
overflow: "visible",
|
|
903
1509
|
} as never,
|
|
904
1510
|
track: {
|
|
905
1511
|
position: "absolute",
|