@onekeyfe/react-native-pager-view 3.0.114 → 3.0.115
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 +40 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +630 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +222 -0
- package/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt +3 -1
- package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
- package/android/src/main/java/com/reactnativepagerview/event/CollapsibleStateChangedEvent.kt +39 -0
- package/ios/RNCCollapsiblePagerViewComponentView.h +9 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +991 -0
- package/ios/RNCPagerViewComponentView.mm +4 -2
- package/lib/module/CollapsiblePagerView.js +167 -0
- package/lib/module/CollapsiblePagerView.web.js +478 -0
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +87 -0
- package/lib/module/PagerView.web.js +177 -0
- package/lib/module/index.js +2 -1
- package/lib/module/index.web.js +6 -0
- package/lib/module/usePagerView.js +1 -1
- package/lib/typescript/src/CollapsiblePagerView.d.ts +58 -0
- package/lib/typescript/src/CollapsiblePagerView.web.d.ts +105 -0
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +51 -0
- package/lib/typescript/src/PagerView.web.d.ts +55 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.web.d.ts +5 -0
- package/package.json +5 -2
- package/src/CollapsiblePagerView.tsx +293 -0
- package/src/CollapsiblePagerView.web.tsx +710 -0
- package/src/CollapsiblePagerViewNativeComponent.ts +87 -0
- package/src/PagerView.web.tsx +280 -0
- package/src/index.tsx +1 -0
- package/src/index.web.tsx +12 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { I18nManager, StyleSheet, View } from "react-native";
|
|
3
|
+
import type * as ReactNative from "react-native";
|
|
4
|
+
|
|
5
|
+
export type OnPageScrollEventData = Readonly<{
|
|
6
|
+
position: number;
|
|
7
|
+
offset: number;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export type OnPageSelectedEventData = Readonly<{
|
|
11
|
+
position: number;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export type OnPageScrollStateChangedEventData = Readonly<{
|
|
15
|
+
pageScrollState: "idle" | "dragging" | "settling";
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export type OnCollapsibleStateChangedEventData = Readonly<{
|
|
19
|
+
position: number;
|
|
20
|
+
headerOffset: number;
|
|
21
|
+
nativePageCount: number;
|
|
22
|
+
attachedPageCount: number;
|
|
23
|
+
observedScrollableCount: number;
|
|
24
|
+
retainedPages: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
interface WebCollapsiblePagerNativeProps extends ReactNative.ViewProps {
|
|
29
|
+
scrollEnabled?: boolean;
|
|
30
|
+
initialPage?: number;
|
|
31
|
+
offscreenPageLimit?: number;
|
|
32
|
+
onPageScroll?: (
|
|
33
|
+
event: ReactNative.NativeSyntheticEvent<OnPageScrollEventData>
|
|
34
|
+
) => void;
|
|
35
|
+
onPageSelected?: (
|
|
36
|
+
event: ReactNative.NativeSyntheticEvent<OnPageSelectedEventData>
|
|
37
|
+
) => void;
|
|
38
|
+
onPageScrollStateChanged?: (
|
|
39
|
+
event: ReactNative.NativeSyntheticEvent<OnPageScrollStateChangedEventData>
|
|
40
|
+
) => void;
|
|
41
|
+
onCollapsibleStateChanged?: (
|
|
42
|
+
event: ReactNative.NativeSyntheticEvent<OnCollapsibleStateChangedEventData>
|
|
43
|
+
) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type CollapsiblePagerDiagnostics =
|
|
47
|
+
ReactNative.NativeSyntheticEvent<OnCollapsibleStateChangedEventData>;
|
|
48
|
+
|
|
49
|
+
export type CollapsiblePagerMountState = Readonly<{
|
|
50
|
+
position: number;
|
|
51
|
+
mountedPages: readonly number[];
|
|
52
|
+
pageKeys: readonly string[];
|
|
53
|
+
}>;
|
|
54
|
+
|
|
55
|
+
export interface CollapsiblePagerViewProps
|
|
56
|
+
extends Omit<
|
|
57
|
+
WebCollapsiblePagerNativeProps,
|
|
58
|
+
| "children"
|
|
59
|
+
| "headerHeight"
|
|
60
|
+
| "stickyHeaderHeight"
|
|
61
|
+
| "pageKeys"
|
|
62
|
+
| "retainedPages"
|
|
63
|
+
| "layoutDirection"
|
|
64
|
+
> {
|
|
65
|
+
header: React.ReactNode;
|
|
66
|
+
stickyHeader: React.ReactNode;
|
|
67
|
+
headerHeight: number;
|
|
68
|
+
stickyHeaderHeight: number;
|
|
69
|
+
pageRetentionDistance?: number;
|
|
70
|
+
layoutDirection?: "ltr" | "rtl" | "locale";
|
|
71
|
+
children?: React.ReactNode;
|
|
72
|
+
onMountedPagesChanged?: (state: CollapsiblePagerMountState) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type State = {
|
|
76
|
+
selectedPage: number;
|
|
77
|
+
animateTransition: boolean;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type ConfiguredScrollElement = Readonly<{
|
|
81
|
+
element: HTMLElement;
|
|
82
|
+
paddingTop: string;
|
|
83
|
+
basePaddingTop: string;
|
|
84
|
+
boxSizing: string;
|
|
85
|
+
scrollTimelineName: string;
|
|
86
|
+
scrollTimelineAxis: string;
|
|
87
|
+
pagerHeaderInset: string;
|
|
88
|
+
pagerStickyInset: string;
|
|
89
|
+
}>;
|
|
90
|
+
|
|
91
|
+
const COLLAPSE_ANIMATION_NAME = "ok-collapsible-pager-collapse";
|
|
92
|
+
const COLLAPSE_STYLE_ID = "ok-collapsible-pager-styles";
|
|
93
|
+
const PAGER_HEADER_INSET = "--ok-collapsible-pager-header-inset";
|
|
94
|
+
const PAGER_STICKY_INSET = "--ok-collapsible-pager-sticky-inset";
|
|
95
|
+
let collapsiblePagerWebInstance = 0;
|
|
96
|
+
|
|
97
|
+
const pageIndex = (value: number) => {
|
|
98
|
+
const index = Math.trunc(value);
|
|
99
|
+
return Number.isFinite(index) ? index : null;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const clampedPageIndex = (value: number, pageCount: number) =>
|
|
103
|
+
Math.min(
|
|
104
|
+
Math.max(0, pageIndex(value) ?? 0),
|
|
105
|
+
Math.max(0, pageCount - 1)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const webElement = (node: unknown) => {
|
|
109
|
+
const element = node as HTMLElement | null;
|
|
110
|
+
return element &&
|
|
111
|
+
typeof element.querySelector === "function" &&
|
|
112
|
+
typeof element.addEventListener === "function"
|
|
113
|
+
? element
|
|
114
|
+
: null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const timelineStyle = (element: HTMLElement) =>
|
|
118
|
+
element.style as CSSStyleDeclaration & {
|
|
119
|
+
scrollTimelineName: string;
|
|
120
|
+
scrollTimelineAxis: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function ensureCollapseAnimation(document: Document) {
|
|
124
|
+
if (document.getElementById(COLLAPSE_STYLE_ID)) return;
|
|
125
|
+
const style = document.createElement("style");
|
|
126
|
+
style.id = COLLAPSE_STYLE_ID;
|
|
127
|
+
style.textContent = `@keyframes ${COLLAPSE_ANIMATION_NAME}{from{transform:translateY(0)}to{transform:translateY(calc(-1 * var(--ok-collapsible-header-height)))}}`;
|
|
128
|
+
document.head.appendChild(style);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const pageKey = (child: React.ReactNode, index: number) => {
|
|
132
|
+
if (React.isValidElement(child) && child.key != null)
|
|
133
|
+
return String(child.key);
|
|
134
|
+
return `page-${index}`;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Web counterpart of the native collapsible pager contract. */
|
|
138
|
+
export class CollapsiblePagerView extends React.PureComponent<
|
|
139
|
+
CollapsiblePagerViewProps,
|
|
140
|
+
State
|
|
141
|
+
> {
|
|
142
|
+
state: State = {
|
|
143
|
+
selectedPage: clampedPageIndex(
|
|
144
|
+
this.props.initialPage ?? 0,
|
|
145
|
+
React.Children.toArray(this.props.children).length
|
|
146
|
+
),
|
|
147
|
+
animateTransition: true,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
private root: HTMLElement | null = null;
|
|
151
|
+
private track: HTMLElement | null = null;
|
|
152
|
+
private touchStartX: number | null = null;
|
|
153
|
+
private touchStartY: number | null = null;
|
|
154
|
+
private horizontalTouch = false;
|
|
155
|
+
private pageOffsets = new Map<string, number>();
|
|
156
|
+
private pageHosts = new Map<number, HTMLElement>();
|
|
157
|
+
private sharedHeaderOffset = 0;
|
|
158
|
+
private scrollEnabled = this.props.scrollEnabled ?? true;
|
|
159
|
+
private lastMountSignature: string | null = null;
|
|
160
|
+
private configureFrame: number | null = null;
|
|
161
|
+
private transitionTimer: ReturnType<typeof setTimeout> | null = null;
|
|
162
|
+
private pageScrollState: OnPageScrollStateChangedEventData["pageScrollState"] =
|
|
163
|
+
"idle";
|
|
164
|
+
private configuredScrollElement: ConfiguredScrollElement | null = null;
|
|
165
|
+
private readonly timelineName = `--ok-collapsible-pager-${++collapsiblePagerWebInstance}`;
|
|
166
|
+
|
|
167
|
+
componentDidMount() {
|
|
168
|
+
if (this.root) ensureCollapseAnimation(this.root.ownerDocument);
|
|
169
|
+
this.scheduleScrollConfiguration();
|
|
170
|
+
this.notifyMountedPagesChanged();
|
|
171
|
+
this.emitDiagnostics("mounted");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
componentDidUpdate(previousProps: CollapsiblePagerViewProps) {
|
|
175
|
+
if (previousProps.scrollEnabled !== this.props.scrollEnabled) {
|
|
176
|
+
this.scrollEnabled = this.props.scrollEnabled ?? true;
|
|
177
|
+
if (!this.scrollEnabled) {
|
|
178
|
+
this.touchStartX = null;
|
|
179
|
+
this.touchStartY = null;
|
|
180
|
+
this.horizontalTouch = false;
|
|
181
|
+
this.finishPageTransition();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const pageCount = this.pages().length;
|
|
185
|
+
const selectedPage = clampedPageIndex(this.state.selectedPage, pageCount);
|
|
186
|
+
if (selectedPage !== this.state.selectedPage) {
|
|
187
|
+
this.finishPageTransition();
|
|
188
|
+
this.setState({
|
|
189
|
+
selectedPage,
|
|
190
|
+
animateTransition: false,
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.scheduleScrollConfiguration();
|
|
195
|
+
this.notifyMountedPagesChanged();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
componentWillUnmount() {
|
|
199
|
+
this.savePageOffset(this.state.selectedPage);
|
|
200
|
+
if (this.configureFrame !== null) {
|
|
201
|
+
cancelAnimationFrame(this.configureFrame);
|
|
202
|
+
this.configureFrame = null;
|
|
203
|
+
}
|
|
204
|
+
if (this.transitionTimer !== null) {
|
|
205
|
+
clearTimeout(this.transitionTimer);
|
|
206
|
+
this.transitionTimer = null;
|
|
207
|
+
}
|
|
208
|
+
this.setTrack(null);
|
|
209
|
+
this.restoreConfiguredScrollElement();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public setPage = (selectedPage: number) => {
|
|
213
|
+
this.selectPage(selectedPage, true);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
public setPageWithoutAnimation = (selectedPage: number) => {
|
|
217
|
+
this.selectPage(selectedPage, false);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
public setScrollEnabled = (scrollEnabled: boolean) => {
|
|
221
|
+
this.scrollEnabled = scrollEnabled;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
private pages() {
|
|
225
|
+
return React.Children.toArray(this.props.children);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private retentionDistance() {
|
|
229
|
+
return Math.max(0, Math.floor(this.props.pageRetentionDistance ?? 1));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private mountedPages(
|
|
233
|
+
pageCount = this.pages().length,
|
|
234
|
+
selected = this.state.selectedPage
|
|
235
|
+
) {
|
|
236
|
+
const mounted: number[] = [];
|
|
237
|
+
const distance = this.retentionDistance();
|
|
238
|
+
for (let index = 0; index < pageCount; index += 1) {
|
|
239
|
+
if (Math.abs(index - selected) <= distance) mounted.push(index);
|
|
240
|
+
}
|
|
241
|
+
return mounted;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private nativeEvent<T>(nativeEvent: T) {
|
|
245
|
+
return { nativeEvent } as ReactNative.NativeSyntheticEvent<T>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private emitPageScrollState(
|
|
249
|
+
pageScrollState: OnPageScrollStateChangedEventData["pageScrollState"]
|
|
250
|
+
) {
|
|
251
|
+
if (pageScrollState === this.pageScrollState) return;
|
|
252
|
+
this.pageScrollState = pageScrollState;
|
|
253
|
+
this.props.onPageScrollStateChanged?.(
|
|
254
|
+
this.nativeEvent<OnPageScrollStateChangedEventData>({ pageScrollState })
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private finishPageTransition = () => {
|
|
259
|
+
if (this.transitionTimer !== null) {
|
|
260
|
+
clearTimeout(this.transitionTimer);
|
|
261
|
+
this.transitionTimer = null;
|
|
262
|
+
}
|
|
263
|
+
this.emitPageScrollState("idle");
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
private beginPageTransition() {
|
|
267
|
+
if (this.transitionTimer !== null) clearTimeout(this.transitionTimer);
|
|
268
|
+
this.emitPageScrollState("settling");
|
|
269
|
+
this.transitionTimer = setTimeout(this.finishPageTransition, 320);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private onTrackTransitionEnd = (event: TransitionEvent) => {
|
|
273
|
+
if (event.target === this.track && event.propertyName === "transform") {
|
|
274
|
+
this.finishPageTransition();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
private setTrack = (node: unknown) => {
|
|
279
|
+
const track = webElement(node);
|
|
280
|
+
if (track === this.track) return;
|
|
281
|
+
this.track?.removeEventListener(
|
|
282
|
+
"transitionend",
|
|
283
|
+
this.onTrackTransitionEnd
|
|
284
|
+
);
|
|
285
|
+
this.track = track;
|
|
286
|
+
this.track?.addEventListener("transitionend", this.onTrackTransitionEnd);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
private pageScrollElement(index: number) {
|
|
290
|
+
const pageHost = this.pageHosts.get(index);
|
|
291
|
+
return (
|
|
292
|
+
pageHost?.querySelector<HTMLElement>(
|
|
293
|
+
".ok-native-list-viewport, [data-collapsible-pager-scroll]"
|
|
294
|
+
) ?? pageHost
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private notifyInsetsChanged(element: HTMLElement) {
|
|
299
|
+
const EventConstructor = element.ownerDocument.defaultView?.Event;
|
|
300
|
+
if (EventConstructor) {
|
|
301
|
+
element.dispatchEvent(
|
|
302
|
+
new EventConstructor("ok-collapsible-pager-insets-changed")
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private restoreConfiguredScrollElement() {
|
|
308
|
+
const configured = this.configuredScrollElement;
|
|
309
|
+
if (!configured) return;
|
|
310
|
+
configured.element.removeEventListener(
|
|
311
|
+
"scrollend",
|
|
312
|
+
this.onVerticalScrollSettled
|
|
313
|
+
);
|
|
314
|
+
const style = timelineStyle(configured.element);
|
|
315
|
+
style.paddingTop = configured.paddingTop;
|
|
316
|
+
style.boxSizing = configured.boxSizing;
|
|
317
|
+
style.scrollTimelineName = configured.scrollTimelineName;
|
|
318
|
+
style.scrollTimelineAxis = configured.scrollTimelineAxis;
|
|
319
|
+
style.setProperty(PAGER_HEADER_INSET, configured.pagerHeaderInset);
|
|
320
|
+
style.setProperty(PAGER_STICKY_INSET, configured.pagerStickyInset);
|
|
321
|
+
configured.element.removeAttribute("data-collapsible-pager-active");
|
|
322
|
+
this.notifyInsetsChanged(configured.element);
|
|
323
|
+
this.configuredScrollElement = null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private configureActiveScrollElement = () => {
|
|
327
|
+
this.configureFrame = null;
|
|
328
|
+
const element = this.pageScrollElement(this.state.selectedPage);
|
|
329
|
+
if (!element) return;
|
|
330
|
+
if (this.configuredScrollElement?.element !== element) {
|
|
331
|
+
this.restoreConfiguredScrollElement();
|
|
332
|
+
const style = timelineStyle(element);
|
|
333
|
+
const computedPaddingTop =
|
|
334
|
+
element.ownerDocument.defaultView?.getComputedStyle(element)
|
|
335
|
+
.paddingTop || style.paddingTop || "0px";
|
|
336
|
+
this.configuredScrollElement = {
|
|
337
|
+
element,
|
|
338
|
+
paddingTop: style.paddingTop,
|
|
339
|
+
basePaddingTop: computedPaddingTop,
|
|
340
|
+
boxSizing: style.boxSizing,
|
|
341
|
+
scrollTimelineName: style.scrollTimelineName,
|
|
342
|
+
scrollTimelineAxis: style.scrollTimelineAxis,
|
|
343
|
+
pagerHeaderInset: style.getPropertyValue(PAGER_HEADER_INSET),
|
|
344
|
+
pagerStickyInset: style.getPropertyValue(PAGER_STICKY_INSET),
|
|
345
|
+
};
|
|
346
|
+
element.addEventListener("scrollend", this.onVerticalScrollSettled);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const style = timelineStyle(element);
|
|
350
|
+
const headerInset = Math.max(0, this.props.headerHeight);
|
|
351
|
+
const stickyInset = Math.max(0, this.props.stickyHeaderHeight);
|
|
352
|
+
const basePaddingTop = this.configuredScrollElement?.basePaddingTop ?? "0px";
|
|
353
|
+
const paddingTop = `calc(${basePaddingTop} + ${headerInset + stickyInset}px)`;
|
|
354
|
+
const headerInsetValue = `${headerInset}px`;
|
|
355
|
+
const stickyInsetValue = `${stickyInset}px`;
|
|
356
|
+
const insetsChanged =
|
|
357
|
+
style.paddingTop !== paddingTop ||
|
|
358
|
+
style.getPropertyValue(PAGER_HEADER_INSET) !== headerInsetValue ||
|
|
359
|
+
style.getPropertyValue(PAGER_STICKY_INSET) !== stickyInsetValue;
|
|
360
|
+
style.paddingTop = paddingTop;
|
|
361
|
+
style.boxSizing = "border-box";
|
|
362
|
+
style.scrollTimelineName = this.timelineName;
|
|
363
|
+
style.scrollTimelineAxis = "block";
|
|
364
|
+
style.setProperty(PAGER_HEADER_INSET, headerInsetValue);
|
|
365
|
+
style.setProperty(PAGER_STICKY_INSET, stickyInsetValue);
|
|
366
|
+
element.setAttribute("data-collapsible-pager-active", "true");
|
|
367
|
+
if (insetsChanged) this.notifyInsetsChanged(element);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
private scheduleScrollConfiguration() {
|
|
371
|
+
if (this.configureFrame !== null) return;
|
|
372
|
+
if (typeof requestAnimationFrame === "function") {
|
|
373
|
+
this.configureFrame = requestAnimationFrame(
|
|
374
|
+
this.configureActiveScrollElement
|
|
375
|
+
);
|
|
376
|
+
} else {
|
|
377
|
+
this.configureActiveScrollElement();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private onVerticalScrollSettled = () => {
|
|
382
|
+
this.savePageOffset(this.state.selectedPage);
|
|
383
|
+
this.emitDiagnostics("scroll-settled");
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
private savePageOffset(index: number) {
|
|
387
|
+
const key = this.pages().map(pageKey)[index];
|
|
388
|
+
const scrollElement = this.pageScrollElement(index);
|
|
389
|
+
if (key && scrollElement) {
|
|
390
|
+
this.pageOffsets.set(key, scrollElement.scrollTop);
|
|
391
|
+
this.sharedHeaderOffset = Math.min(
|
|
392
|
+
Math.max(scrollElement.scrollTop, 0),
|
|
393
|
+
Math.max(0, this.props.headerHeight)
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private restorePageOffset(index: number) {
|
|
399
|
+
const key = this.pages().map(pageKey)[index];
|
|
400
|
+
const scrollElement = this.pageScrollElement(index);
|
|
401
|
+
if (!key || !scrollElement) return;
|
|
402
|
+
scrollElement.scrollTop = Math.max(
|
|
403
|
+
this.pageOffsets.get(key) ?? this.sharedHeaderOffset,
|
|
404
|
+
this.sharedHeaderOffset
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private selectPage(selectedPage: number, animated: boolean) {
|
|
409
|
+
const pages = this.pages();
|
|
410
|
+
const position = pageIndex(selectedPage);
|
|
411
|
+
if (
|
|
412
|
+
position === null ||
|
|
413
|
+
position < 0 ||
|
|
414
|
+
position >= pages.length ||
|
|
415
|
+
position === this.state.selectedPage
|
|
416
|
+
) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
this.savePageOffset(this.state.selectedPage);
|
|
420
|
+
|
|
421
|
+
if (animated) this.beginPageTransition();
|
|
422
|
+
else this.finishPageTransition();
|
|
423
|
+
this.setState({ selectedPage: position, animateTransition: animated }, () => {
|
|
424
|
+
this.scheduleScrollConfiguration();
|
|
425
|
+
const restore = () => {
|
|
426
|
+
this.configureActiveScrollElement();
|
|
427
|
+
this.restorePageOffset(position);
|
|
428
|
+
};
|
|
429
|
+
if (typeof requestAnimationFrame === "function") {
|
|
430
|
+
requestAnimationFrame(restore);
|
|
431
|
+
} else {
|
|
432
|
+
setTimeout(restore, 0);
|
|
433
|
+
}
|
|
434
|
+
this.props.onPageSelected?.(
|
|
435
|
+
this.nativeEvent<OnPageSelectedEventData>({ position })
|
|
436
|
+
);
|
|
437
|
+
this.props.onPageScroll?.(
|
|
438
|
+
this.nativeEvent<OnPageScrollEventData>({
|
|
439
|
+
position,
|
|
440
|
+
offset: 0,
|
|
441
|
+
})
|
|
442
|
+
);
|
|
443
|
+
this.notifyMountedPagesChanged();
|
|
444
|
+
this.emitDiagnostics("page-selected");
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private notifyMountedPagesChanged = () => {
|
|
449
|
+
if (!this.props.onMountedPagesChanged) return;
|
|
450
|
+
const pages = this.pages();
|
|
451
|
+
const state = {
|
|
452
|
+
position: this.state.selectedPage,
|
|
453
|
+
mountedPages: this.mountedPages(pages.length),
|
|
454
|
+
pageKeys: pages.map(pageKey),
|
|
455
|
+
};
|
|
456
|
+
const signature = JSON.stringify(state);
|
|
457
|
+
if (signature === this.lastMountSignature) return;
|
|
458
|
+
this.lastMountSignature = signature;
|
|
459
|
+
this.props.onMountedPagesChanged(state);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
private emitDiagnostics(reason: string) {
|
|
463
|
+
const pages = this.pages();
|
|
464
|
+
const retainedPages = this.mountedPages(pages.length);
|
|
465
|
+
this.props.onCollapsibleStateChanged?.(
|
|
466
|
+
this.nativeEvent<OnCollapsibleStateChangedEventData>({
|
|
467
|
+
position: this.state.selectedPage,
|
|
468
|
+
headerOffset: Math.min(
|
|
469
|
+
Math.max(
|
|
470
|
+
this.pageScrollElement(this.state.selectedPage)?.scrollTop ?? 0,
|
|
471
|
+
0
|
|
472
|
+
),
|
|
473
|
+
Math.max(0, this.props.headerHeight)
|
|
474
|
+
),
|
|
475
|
+
nativePageCount: pages.length,
|
|
476
|
+
attachedPageCount: retainedPages.length,
|
|
477
|
+
observedScrollableCount: retainedPages.reduce(
|
|
478
|
+
(count, index) => count + (this.pageScrollElement(index) ? 1 : 0),
|
|
479
|
+
0
|
|
480
|
+
),
|
|
481
|
+
retainedPages: JSON.stringify(retainedPages),
|
|
482
|
+
reason,
|
|
483
|
+
})
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private onTouchStart = (event: ReactNative.GestureResponderEvent) => {
|
|
488
|
+
if (!this.scrollEnabled) return;
|
|
489
|
+
const touch = event.nativeEvent.touches[0];
|
|
490
|
+
if (!touch) return;
|
|
491
|
+
this.touchStartX = touch.pageX;
|
|
492
|
+
this.touchStartY = touch.pageY;
|
|
493
|
+
this.horizontalTouch = false;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
private onTouchMove = (event: ReactNative.GestureResponderEvent) => {
|
|
497
|
+
if (this.touchStartX == null || !this.scrollEnabled || this.horizontalTouch)
|
|
498
|
+
return;
|
|
499
|
+
const touch = event.nativeEvent.touches[0];
|
|
500
|
+
if (!touch) return;
|
|
501
|
+
const deltaX = touch.pageX - this.touchStartX;
|
|
502
|
+
const deltaY = touch.pageY - (this.touchStartY ?? touch.pageY);
|
|
503
|
+
if (Math.abs(deltaX) >= 8 && Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
504
|
+
this.horizontalTouch = true;
|
|
505
|
+
this.emitPageScrollState("dragging");
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
private onTouchEnd = (event: ReactNative.GestureResponderEvent) => {
|
|
510
|
+
if (this.touchStartX == null || !this.scrollEnabled) return;
|
|
511
|
+
const touch = event.nativeEvent.changedTouches[0];
|
|
512
|
+
if (!touch) {
|
|
513
|
+
this.onTouchCancel();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const delta = touch.pageX - this.touchStartX;
|
|
517
|
+
const verticalDelta = touch.pageY - (this.touchStartY ?? touch.pageY);
|
|
518
|
+
this.touchStartX = null;
|
|
519
|
+
this.touchStartY = null;
|
|
520
|
+
const rtl =
|
|
521
|
+
this.props.layoutDirection === "rtl" ||
|
|
522
|
+
((!this.props.layoutDirection ||
|
|
523
|
+
this.props.layoutDirection === "locale") &&
|
|
524
|
+
I18nManager.isRTL);
|
|
525
|
+
const direction = Math.abs(delta) >= 40 && Math.abs(delta) > Math.abs(verticalDelta)
|
|
526
|
+
? (delta < 0 ? 1 : -1)
|
|
527
|
+
: 0;
|
|
528
|
+
const next = this.state.selectedPage + (rtl ? -direction : direction);
|
|
529
|
+
if (direction === 0 || next < 0 || next >= this.pages().length) {
|
|
530
|
+
const wasHorizontal = this.horizontalTouch;
|
|
531
|
+
this.horizontalTouch = false;
|
|
532
|
+
if (wasHorizontal) this.finishPageTransition();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
this.horizontalTouch = false;
|
|
536
|
+
this.selectPage(next, true);
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
private onTouchCancel = () => {
|
|
540
|
+
const wasHorizontal = this.horizontalTouch;
|
|
541
|
+
this.touchStartX = null;
|
|
542
|
+
this.touchStartY = null;
|
|
543
|
+
this.horizontalTouch = false;
|
|
544
|
+
if (wasHorizontal) this.finishPageTransition();
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
render() {
|
|
548
|
+
const {
|
|
549
|
+
children: _children,
|
|
550
|
+
header,
|
|
551
|
+
stickyHeader,
|
|
552
|
+
headerHeight: _headerHeight,
|
|
553
|
+
stickyHeaderHeight: _stickyHeaderHeight,
|
|
554
|
+
pageRetentionDistance: _pageRetentionDistance,
|
|
555
|
+
layoutDirection: _layoutDirection,
|
|
556
|
+
scrollEnabled: _scrollEnabled,
|
|
557
|
+
initialPage: _initialPage,
|
|
558
|
+
offscreenPageLimit: _offscreenPageLimit,
|
|
559
|
+
onPageScroll: _onPageScroll,
|
|
560
|
+
onPageSelected: _onPageSelected,
|
|
561
|
+
onPageScrollStateChanged: _onPageScrollStateChanged,
|
|
562
|
+
onCollapsibleStateChanged: _onCollapsibleStateChanged,
|
|
563
|
+
onMountedPagesChanged: _onMountedPagesChanged,
|
|
564
|
+
style,
|
|
565
|
+
...viewProps
|
|
566
|
+
} = this.props;
|
|
567
|
+
const pages = this.pages();
|
|
568
|
+
const keys = pages.map(pageKey);
|
|
569
|
+
const retained = new Set(this.mountedPages(pages.length));
|
|
570
|
+
const pageWidth = pages.length > 0 ? `${100 / pages.length}%` : "100%";
|
|
571
|
+
const collapseAnimation = {
|
|
572
|
+
"--ok-collapsible-header-height": `${Math.max(
|
|
573
|
+
0,
|
|
574
|
+
this.props.headerHeight
|
|
575
|
+
)}px`,
|
|
576
|
+
animationName: COLLAPSE_ANIMATION_NAME,
|
|
577
|
+
animationDuration: "auto",
|
|
578
|
+
animationFillMode: "both",
|
|
579
|
+
animationTimingFunction: "linear",
|
|
580
|
+
animationTimeline: this.timelineName,
|
|
581
|
+
animationRange: `0px ${Math.max(1, this.props.headerHeight)}px`,
|
|
582
|
+
} as never;
|
|
583
|
+
const trackStyle = [
|
|
584
|
+
styles.track,
|
|
585
|
+
{
|
|
586
|
+
width: `${Math.max(1, pages.length) * 100}%`,
|
|
587
|
+
transform: [
|
|
588
|
+
{
|
|
589
|
+
translateX: `${
|
|
590
|
+
(-this.state.selectedPage * 100) / Math.max(1, pages.length)
|
|
591
|
+
}%`,
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
transitionProperty: "transform",
|
|
595
|
+
transitionDuration: this.state.animateTransition ? "240ms" : "0ms",
|
|
596
|
+
},
|
|
597
|
+
] as never;
|
|
598
|
+
|
|
599
|
+
return (
|
|
600
|
+
<View
|
|
601
|
+
{...viewProps}
|
|
602
|
+
ref={(node) => {
|
|
603
|
+
this.root = webElement(node);
|
|
604
|
+
}}
|
|
605
|
+
style={[
|
|
606
|
+
styles.root,
|
|
607
|
+
{ timelineScope: this.timelineName } as never,
|
|
608
|
+
style,
|
|
609
|
+
]}
|
|
610
|
+
>
|
|
611
|
+
<View
|
|
612
|
+
style={[
|
|
613
|
+
styles.header,
|
|
614
|
+
{ height: Math.max(0, this.props.headerHeight) },
|
|
615
|
+
collapseAnimation,
|
|
616
|
+
]}
|
|
617
|
+
>
|
|
618
|
+
{header}
|
|
619
|
+
</View>
|
|
620
|
+
<View
|
|
621
|
+
style={[
|
|
622
|
+
styles.sticky,
|
|
623
|
+
{
|
|
624
|
+
top: Math.max(0, this.props.headerHeight),
|
|
625
|
+
height: Math.max(0, this.props.stickyHeaderHeight),
|
|
626
|
+
},
|
|
627
|
+
collapseAnimation,
|
|
628
|
+
]}
|
|
629
|
+
>
|
|
630
|
+
{stickyHeader}
|
|
631
|
+
</View>
|
|
632
|
+
<View
|
|
633
|
+
ref={this.setTrack}
|
|
634
|
+
style={trackStyle}
|
|
635
|
+
onTouchStart={this.onTouchStart}
|
|
636
|
+
onTouchMove={this.onTouchMove}
|
|
637
|
+
onTouchEnd={this.onTouchEnd}
|
|
638
|
+
onTouchCancel={this.onTouchCancel}
|
|
639
|
+
>
|
|
640
|
+
{pages.map((page, index) => (
|
|
641
|
+
<View
|
|
642
|
+
key={keys[index]}
|
|
643
|
+
ref={(node) => {
|
|
644
|
+
const host = webElement(node);
|
|
645
|
+
if (host) {
|
|
646
|
+
host.inert = index !== this.state.selectedPage;
|
|
647
|
+
this.pageHosts.set(index, host);
|
|
648
|
+
} else {
|
|
649
|
+
this.pageHosts.delete(index);
|
|
650
|
+
}
|
|
651
|
+
}}
|
|
652
|
+
style={[styles.page, { width: pageWidth }]}
|
|
653
|
+
aria-hidden={index !== this.state.selectedPage}
|
|
654
|
+
>
|
|
655
|
+
{retained.has(index) ? page : null}
|
|
656
|
+
</View>
|
|
657
|
+
))}
|
|
658
|
+
</View>
|
|
659
|
+
</View>
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const styles = StyleSheet.create({
|
|
665
|
+
root: {
|
|
666
|
+
flex: 1,
|
|
667
|
+
overflow: "hidden",
|
|
668
|
+
position: "relative",
|
|
669
|
+
} as never,
|
|
670
|
+
header: {
|
|
671
|
+
position: "absolute",
|
|
672
|
+
left: 0,
|
|
673
|
+
right: 0,
|
|
674
|
+
top: 0,
|
|
675
|
+
zIndex: 3,
|
|
676
|
+
overflow: "hidden",
|
|
677
|
+
} as never,
|
|
678
|
+
sticky: {
|
|
679
|
+
position: "absolute",
|
|
680
|
+
left: 0,
|
|
681
|
+
right: 0,
|
|
682
|
+
zIndex: 4,
|
|
683
|
+
overflow: "hidden",
|
|
684
|
+
} as never,
|
|
685
|
+
track: {
|
|
686
|
+
position: "absolute",
|
|
687
|
+
inset: 0,
|
|
688
|
+
display: "flex",
|
|
689
|
+
flexDirection: "row",
|
|
690
|
+
alignItems: "stretch",
|
|
691
|
+
touchAction: "pan-y",
|
|
692
|
+
} as never,
|
|
693
|
+
page: {
|
|
694
|
+
flexShrink: 0,
|
|
695
|
+
minWidth: 0,
|
|
696
|
+
minHeight: 0,
|
|
697
|
+
height: "100%",
|
|
698
|
+
display: "flex",
|
|
699
|
+
position: "relative",
|
|
700
|
+
overflowY: "auto",
|
|
701
|
+
overflowX: "hidden",
|
|
702
|
+
} as never,
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
export type CollapsiblePagerViewOnPageScrollEvent =
|
|
706
|
+
ReactNative.NativeSyntheticEvent<OnPageScrollEventData>;
|
|
707
|
+
export type CollapsiblePagerViewOnPageSelectedEvent =
|
|
708
|
+
ReactNative.NativeSyntheticEvent<OnPageSelectedEventData>;
|
|
709
|
+
export type CollapsiblePagerViewOnPageScrollStateChangedEvent =
|
|
710
|
+
ReactNative.NativeSyntheticEvent<OnPageScrollStateChangedEventData>;
|