@onekeyfe/react-native-pager-view 3.0.172 → 3.0.174
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/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +506 -56
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +4 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +14 -0
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +2 -0
- package/docs/SPEC.md +54 -48
- package/ios/RNCCollapsiblePagerViewComponentView.mm +245 -32
- package/ios/RNCNativeScrollerComponentView.mm +6 -0
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +85 -49
- package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +14 -5
- package/lib/module/CollapsiblePagerView.js +6 -0
- package/lib/module/CollapsiblePagerView.web.js +81 -0
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +6 -0
- package/lib/typescript/src/CollapsiblePagerView.d.ts +6 -0
- package/lib/typescript/src/CollapsiblePagerView.web.d.ts +11 -0
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +1 -0
- package/package.json +2 -2
- package/src/CollapsiblePagerView.tsx +18 -0
- package/src/CollapsiblePagerView.web.tsx +170 -36
- package/src/CollapsiblePagerViewNativeComponent.ts +6 -0
|
@@ -79,6 +79,8 @@ type State = {
|
|
|
79
79
|
|
|
80
80
|
type ConfiguredScrollElement = Readonly<{
|
|
81
81
|
element: HTMLElement;
|
|
82
|
+
rangeSpacer: HTMLElement;
|
|
83
|
+
position: string;
|
|
82
84
|
paddingTop: string;
|
|
83
85
|
basePaddingTop: string;
|
|
84
86
|
boxSizing: string;
|
|
@@ -100,10 +102,7 @@ const pageIndex = (value: number) => {
|
|
|
100
102
|
};
|
|
101
103
|
|
|
102
104
|
const clampedPageIndex = (value: number, pageCount: number) =>
|
|
103
|
-
Math.min(
|
|
104
|
-
Math.max(0, pageIndex(value) ?? 0),
|
|
105
|
-
Math.max(0, pageCount - 1)
|
|
106
|
-
);
|
|
105
|
+
Math.min(Math.max(0, pageIndex(value) ?? 0), Math.max(0, pageCount - 1));
|
|
107
106
|
|
|
108
107
|
const webElement = (node: unknown) => {
|
|
109
108
|
const element = node as HTMLElement | null;
|
|
@@ -158,6 +157,11 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
158
157
|
private scrollEnabled = this.props.scrollEnabled ?? true;
|
|
159
158
|
private lastMountSignature: string | null = null;
|
|
160
159
|
private configureFrame: number | null = null;
|
|
160
|
+
private pendingTopFrame: Readonly<{
|
|
161
|
+
frame: number;
|
|
162
|
+
element: HTMLElement;
|
|
163
|
+
key: string;
|
|
164
|
+
}> | null = null;
|
|
161
165
|
private transitionTimer: ReturnType<typeof setTimeout> | null = null;
|
|
162
166
|
private pageScrollState: OnPageScrollStateChangedEventData["pageScrollState"] =
|
|
163
167
|
"idle";
|
|
@@ -184,6 +188,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
184
188
|
const pageCount = this.pages().length;
|
|
185
189
|
const selectedPage = clampedPageIndex(this.state.selectedPage, pageCount);
|
|
186
190
|
if (selectedPage !== this.state.selectedPage) {
|
|
191
|
+
this.cancelPendingTopFrame();
|
|
187
192
|
this.finishPageTransition();
|
|
188
193
|
this.setState({
|
|
189
194
|
selectedPage,
|
|
@@ -196,6 +201,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
componentWillUnmount() {
|
|
204
|
+
this.cancelPendingTopFrame();
|
|
199
205
|
this.savePageOffset(this.state.selectedPage);
|
|
200
206
|
if (this.configureFrame !== null) {
|
|
201
207
|
cancelAnimationFrame(this.configureFrame);
|
|
@@ -217,6 +223,85 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
217
223
|
this.selectPage(selectedPage, false);
|
|
218
224
|
};
|
|
219
225
|
|
|
226
|
+
public scrollToPageTop = (options?: Readonly<{ animated?: boolean }>) => {
|
|
227
|
+
this.scrollCurrentPageToTop(0, options?.animated ?? true);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
public scrollToStickyTop = (options?: Readonly<{ animated?: boolean }>) => {
|
|
231
|
+
this.scrollCurrentPageToTop(
|
|
232
|
+
Math.max(0, this.props.headerHeight),
|
|
233
|
+
options?.animated ?? true
|
|
234
|
+
);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
private cancelPendingTopFrame = () => {
|
|
238
|
+
if (!this.pendingTopFrame) return;
|
|
239
|
+
const pending = this.pendingTopFrame;
|
|
240
|
+
cancelAnimationFrame(pending.frame);
|
|
241
|
+
this.pendingTopFrame = null;
|
|
242
|
+
if (
|
|
243
|
+
this.pageScrollElement(this.state.selectedPage) === pending.element &&
|
|
244
|
+
this.pages().map(pageKey)[this.state.selectedPage] === pending.key
|
|
245
|
+
)
|
|
246
|
+
this.savePageOffset(this.state.selectedPage);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
private onQueuedTopWheel = (event: WheelEvent) => {
|
|
250
|
+
if (event.deltaY !== 0) this.cancelPendingTopFrame();
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
private onQueuedTopKey = (event: KeyboardEvent) => {
|
|
254
|
+
if (
|
|
255
|
+
[
|
|
256
|
+
"ArrowUp",
|
|
257
|
+
"ArrowDown",
|
|
258
|
+
"PageUp",
|
|
259
|
+
"PageDown",
|
|
260
|
+
"Home",
|
|
261
|
+
"End",
|
|
262
|
+
" ",
|
|
263
|
+
].includes(event.key)
|
|
264
|
+
)
|
|
265
|
+
this.cancelPendingTopFrame();
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
private scrollCurrentPageToTop(offset: number, animated: boolean) {
|
|
269
|
+
if (this.horizontalTouch || this.transitionTimer !== null) return;
|
|
270
|
+
const element = this.pageScrollElement(this.state.selectedPage);
|
|
271
|
+
const key = this.pages().map(pageKey)[this.state.selectedPage];
|
|
272
|
+
if (!element?.isConnected || !key) return;
|
|
273
|
+
this.cancelPendingTopFrame();
|
|
274
|
+
const EventConstructor = element.ownerDocument.defaultView?.Event;
|
|
275
|
+
if (EventConstructor)
|
|
276
|
+
element.dispatchEvent(
|
|
277
|
+
new EventConstructor("ok-collapsible-pager-cancel-scroll-command")
|
|
278
|
+
);
|
|
279
|
+
element.scrollTo({
|
|
280
|
+
left: element.scrollLeft,
|
|
281
|
+
top: element.scrollTop,
|
|
282
|
+
behavior: "instant",
|
|
283
|
+
});
|
|
284
|
+
const frame = requestAnimationFrame(() => {
|
|
285
|
+
if (this.pendingTopFrame?.frame !== frame) return;
|
|
286
|
+
this.pendingTopFrame = null;
|
|
287
|
+
if (
|
|
288
|
+
!element.isConnected ||
|
|
289
|
+
this.horizontalTouch ||
|
|
290
|
+
this.transitionTimer !== null ||
|
|
291
|
+
this.pageScrollElement(this.state.selectedPage) !== element ||
|
|
292
|
+
this.pages().map(pageKey)[this.state.selectedPage] !== key
|
|
293
|
+
)
|
|
294
|
+
return;
|
|
295
|
+
this.pageOffsets.set(key, offset);
|
|
296
|
+
this.sharedHeaderOffset = offset;
|
|
297
|
+
element.scrollTo({
|
|
298
|
+
top: offset,
|
|
299
|
+
behavior: animated ? "smooth" : "instant",
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
this.pendingTopFrame = { frame, element, key };
|
|
303
|
+
}
|
|
304
|
+
|
|
220
305
|
public setScrollEnabled = (scrollEnabled: boolean) => {
|
|
221
306
|
this.scrollEnabled = scrollEnabled;
|
|
222
307
|
};
|
|
@@ -278,10 +363,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
278
363
|
private setTrack = (node: unknown) => {
|
|
279
364
|
const track = webElement(node);
|
|
280
365
|
if (track === this.track) return;
|
|
281
|
-
this.track?.removeEventListener(
|
|
282
|
-
"transitionend",
|
|
283
|
-
this.onTrackTransitionEnd
|
|
284
|
-
);
|
|
366
|
+
this.track?.removeEventListener("transitionend", this.onTrackTransitionEnd);
|
|
285
367
|
this.track = track;
|
|
286
368
|
this.track?.addEventListener("transitionend", this.onTrackTransitionEnd);
|
|
287
369
|
};
|
|
@@ -307,11 +389,20 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
307
389
|
private restoreConfiguredScrollElement() {
|
|
308
390
|
const configured = this.configuredScrollElement;
|
|
309
391
|
if (!configured) return;
|
|
392
|
+
this.cancelPendingTopFrame();
|
|
393
|
+
configured.element.removeEventListener(
|
|
394
|
+
"ok-native-list-scroll-command-accepted",
|
|
395
|
+
this.cancelPendingTopFrame
|
|
396
|
+
);
|
|
397
|
+
configured.element.removeEventListener("wheel", this.onQueuedTopWheel);
|
|
398
|
+
configured.element.removeEventListener("keydown", this.onQueuedTopKey);
|
|
310
399
|
configured.element.removeEventListener(
|
|
311
400
|
"scrollend",
|
|
312
401
|
this.onVerticalScrollSettled
|
|
313
402
|
);
|
|
314
403
|
const style = timelineStyle(configured.element);
|
|
404
|
+
configured.rangeSpacer.remove();
|
|
405
|
+
style.position = configured.position;
|
|
315
406
|
style.paddingTop = configured.paddingTop;
|
|
316
407
|
style.boxSizing = configured.boxSizing;
|
|
317
408
|
style.scrollTimelineName = configured.scrollTimelineName;
|
|
@@ -332,9 +423,30 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
332
423
|
const style = timelineStyle(element);
|
|
333
424
|
const computedPaddingTop =
|
|
334
425
|
element.ownerDocument.defaultView?.getComputedStyle(element)
|
|
335
|
-
.paddingTop ||
|
|
426
|
+
.paddingTop ||
|
|
427
|
+
style.paddingTop ||
|
|
428
|
+
"0px";
|
|
429
|
+
const rangeSpacer = element.ownerDocument.createElement("div");
|
|
430
|
+
Object.assign(rangeSpacer.style, {
|
|
431
|
+
position: "absolute",
|
|
432
|
+
top: "0",
|
|
433
|
+
left: "0",
|
|
434
|
+
width: "1px",
|
|
435
|
+
pointerEvents: "none",
|
|
436
|
+
opacity: "0",
|
|
437
|
+
});
|
|
438
|
+
rangeSpacer.setAttribute("aria-hidden", "true");
|
|
439
|
+
const position = style.position;
|
|
440
|
+
if (
|
|
441
|
+
element.ownerDocument.defaultView?.getComputedStyle(element)
|
|
442
|
+
.position === "static"
|
|
443
|
+
)
|
|
444
|
+
style.position = "relative";
|
|
445
|
+
element.appendChild(rangeSpacer);
|
|
336
446
|
this.configuredScrollElement = {
|
|
337
447
|
element,
|
|
448
|
+
rangeSpacer,
|
|
449
|
+
position,
|
|
338
450
|
paddingTop: style.paddingTop,
|
|
339
451
|
basePaddingTop: computedPaddingTop,
|
|
340
452
|
boxSizing: style.boxSizing,
|
|
@@ -344,13 +456,24 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
344
456
|
pagerStickyInset: style.getPropertyValue(PAGER_STICKY_INSET),
|
|
345
457
|
};
|
|
346
458
|
element.addEventListener("scrollend", this.onVerticalScrollSettled);
|
|
459
|
+
element.addEventListener(
|
|
460
|
+
"ok-native-list-scroll-command-accepted",
|
|
461
|
+
this.cancelPendingTopFrame
|
|
462
|
+
);
|
|
463
|
+
element.addEventListener("wheel", this.onQueuedTopWheel);
|
|
464
|
+
element.addEventListener("keydown", this.onQueuedTopKey);
|
|
347
465
|
}
|
|
348
466
|
|
|
349
467
|
const style = timelineStyle(element);
|
|
350
468
|
const headerInset = Math.max(0, this.props.headerHeight);
|
|
351
469
|
const stickyInset = Math.max(0, this.props.stickyHeaderHeight);
|
|
352
|
-
|
|
353
|
-
|
|
470
|
+
if (this.configuredScrollElement)
|
|
471
|
+
this.configuredScrollElement.rangeSpacer.style.height = `calc(100% + ${headerInset}px)`;
|
|
472
|
+
const basePaddingTop =
|
|
473
|
+
this.configuredScrollElement?.basePaddingTop ?? "0px";
|
|
474
|
+
const paddingTop = `calc(${basePaddingTop} + ${
|
|
475
|
+
headerInset + stickyInset
|
|
476
|
+
}px)`;
|
|
354
477
|
const headerInsetValue = `${headerInset}px`;
|
|
355
478
|
const stickyInsetValue = `${stickyInset}px`;
|
|
356
479
|
const insetsChanged =
|
|
@@ -379,6 +502,7 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
379
502
|
}
|
|
380
503
|
|
|
381
504
|
private onVerticalScrollSettled = () => {
|
|
505
|
+
if (this.pendingTopFrame) return;
|
|
382
506
|
this.savePageOffset(this.state.selectedPage);
|
|
383
507
|
this.emitDiagnostics("scroll-settled");
|
|
384
508
|
};
|
|
@@ -416,33 +540,37 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
416
540
|
) {
|
|
417
541
|
return;
|
|
418
542
|
}
|
|
543
|
+
this.cancelPendingTopFrame();
|
|
419
544
|
this.savePageOffset(this.state.selectedPage);
|
|
420
545
|
|
|
421
546
|
if (animated) this.beginPageTransition();
|
|
422
547
|
else this.finishPageTransition();
|
|
423
|
-
this.setState(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
this.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
548
|
+
this.setState(
|
|
549
|
+
{ selectedPage: position, animateTransition: animated },
|
|
550
|
+
() => {
|
|
551
|
+
this.scheduleScrollConfiguration();
|
|
552
|
+
const restore = () => {
|
|
553
|
+
this.configureActiveScrollElement();
|
|
554
|
+
this.restorePageOffset(position);
|
|
555
|
+
};
|
|
556
|
+
if (typeof requestAnimationFrame === "function") {
|
|
557
|
+
requestAnimationFrame(restore);
|
|
558
|
+
} else {
|
|
559
|
+
setTimeout(restore, 0);
|
|
560
|
+
}
|
|
561
|
+
this.props.onPageSelected?.(
|
|
562
|
+
this.nativeEvent<OnPageSelectedEventData>({ position })
|
|
563
|
+
);
|
|
564
|
+
this.props.onPageScroll?.(
|
|
565
|
+
this.nativeEvent<OnPageScrollEventData>({
|
|
566
|
+
position,
|
|
567
|
+
offset: 0,
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
this.notifyMountedPagesChanged();
|
|
571
|
+
this.emitDiagnostics("page-selected");
|
|
433
572
|
}
|
|
434
|
-
|
|
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
|
-
});
|
|
573
|
+
);
|
|
446
574
|
}
|
|
447
575
|
|
|
448
576
|
private notifyMountedPagesChanged = () => {
|
|
@@ -500,7 +628,10 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
500
628
|
if (!touch) return;
|
|
501
629
|
const deltaX = touch.pageX - this.touchStartX;
|
|
502
630
|
const deltaY = touch.pageY - (this.touchStartY ?? touch.pageY);
|
|
631
|
+
if (Math.abs(deltaY) >= 8 && Math.abs(deltaY) > Math.abs(deltaX))
|
|
632
|
+
this.cancelPendingTopFrame();
|
|
503
633
|
if (Math.abs(deltaX) >= 8 && Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
634
|
+
this.cancelPendingTopFrame();
|
|
504
635
|
this.horizontalTouch = true;
|
|
505
636
|
this.emitPageScrollState("dragging");
|
|
506
637
|
}
|
|
@@ -522,9 +653,12 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
522
653
|
((!this.props.layoutDirection ||
|
|
523
654
|
this.props.layoutDirection === "locale") &&
|
|
524
655
|
I18nManager.isRTL);
|
|
525
|
-
const direction =
|
|
526
|
-
|
|
527
|
-
|
|
656
|
+
const direction =
|
|
657
|
+
Math.abs(delta) >= 40 && Math.abs(delta) > Math.abs(verticalDelta)
|
|
658
|
+
? delta < 0
|
|
659
|
+
? 1
|
|
660
|
+
: -1
|
|
661
|
+
: 0;
|
|
528
662
|
const next = this.state.selectedPage + (rtl ? -direction : direction);
|
|
529
663
|
if (direction === 0 || next < 0 || next >= this.pages().length) {
|
|
530
664
|
const wasHorizontal = this.horizontalTouch;
|
|
@@ -83,6 +83,11 @@ export interface NativeProps extends ViewProps {
|
|
|
83
83
|
type CollapsiblePagerViewNativeType = HostComponent<NativeProps>;
|
|
84
84
|
|
|
85
85
|
export interface NativeCommands {
|
|
86
|
+
scrollToTop: (
|
|
87
|
+
viewRef: React.ElementRef<CollapsiblePagerViewNativeType>,
|
|
88
|
+
target: Int32,
|
|
89
|
+
animated: boolean
|
|
90
|
+
) => void;
|
|
86
91
|
setPage: (
|
|
87
92
|
viewRef: React.ElementRef<CollapsiblePagerViewNativeType>,
|
|
88
93
|
selectedPage: Int32
|
|
@@ -99,6 +104,7 @@ export interface NativeCommands {
|
|
|
99
104
|
|
|
100
105
|
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
|
101
106
|
supportedCommands: [
|
|
107
|
+
"scrollToTop",
|
|
102
108
|
"setPage",
|
|
103
109
|
"setPageWithoutAnimation",
|
|
104
110
|
"setScrollEnabledImperatively",
|