@onekeyfe/react-native-pager-view 3.0.113 → 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.
@@ -80,15 +80,40 @@ type State = {
80
80
  type ConfiguredScrollElement = Readonly<{
81
81
  element: HTMLElement;
82
82
  paddingTop: string;
83
+ basePaddingTop: string;
83
84
  boxSizing: string;
84
85
  scrollTimelineName: string;
85
86
  scrollTimelineAxis: string;
87
+ pagerHeaderInset: string;
88
+ pagerStickyInset: string;
86
89
  }>;
87
90
 
88
91
  const COLLAPSE_ANIMATION_NAME = "ok-collapsible-pager-collapse";
89
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";
90
95
  let collapsiblePagerWebInstance = 0;
91
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
+
92
117
  const timelineStyle = (element: HTMLElement) =>
93
118
  element.style as CSSStyleDeclaration & {
94
119
  scrollTimelineName: string;
@@ -115,19 +140,27 @@ export class CollapsiblePagerView extends React.PureComponent<
115
140
  State
116
141
  > {
117
142
  state: State = {
118
- selectedPage: Math.max(0, this.props.initialPage ?? 0),
143
+ selectedPage: clampedPageIndex(
144
+ this.props.initialPage ?? 0,
145
+ React.Children.toArray(this.props.children).length
146
+ ),
119
147
  animateTransition: true,
120
148
  };
121
149
 
122
150
  private root: HTMLElement | null = null;
151
+ private track: HTMLElement | null = null;
123
152
  private touchStartX: number | null = null;
124
153
  private touchStartY: number | null = null;
154
+ private horizontalTouch = false;
125
155
  private pageOffsets = new Map<string, number>();
126
156
  private pageHosts = new Map<number, HTMLElement>();
127
157
  private sharedHeaderOffset = 0;
128
158
  private scrollEnabled = this.props.scrollEnabled ?? true;
129
159
  private lastMountSignature: string | null = null;
130
160
  private configureFrame: number | null = null;
161
+ private transitionTimer: ReturnType<typeof setTimeout> | null = null;
162
+ private pageScrollState: OnPageScrollStateChangedEventData["pageScrollState"] =
163
+ "idle";
131
164
  private configuredScrollElement: ConfiguredScrollElement | null = null;
132
165
  private readonly timelineName = `--ok-collapsible-pager-${++collapsiblePagerWebInstance}`;
133
166
 
@@ -141,6 +174,22 @@ export class CollapsiblePagerView extends React.PureComponent<
141
174
  componentDidUpdate(previousProps: CollapsiblePagerViewProps) {
142
175
  if (previousProps.scrollEnabled !== this.props.scrollEnabled) {
143
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;
144
193
  }
145
194
  this.scheduleScrollConfiguration();
146
195
  this.notifyMountedPagesChanged();
@@ -152,6 +201,11 @@ export class CollapsiblePagerView extends React.PureComponent<
152
201
  cancelAnimationFrame(this.configureFrame);
153
202
  this.configureFrame = null;
154
203
  }
204
+ if (this.transitionTimer !== null) {
205
+ clearTimeout(this.transitionTimer);
206
+ this.transitionTimer = null;
207
+ }
208
+ this.setTrack(null);
155
209
  this.restoreConfiguredScrollElement();
156
210
  }
157
211
 
@@ -191,6 +245,47 @@ export class CollapsiblePagerView extends React.PureComponent<
191
245
  return { nativeEvent } as ReactNative.NativeSyntheticEvent<T>;
192
246
  }
193
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
+
194
289
  private pageScrollElement(index: number) {
195
290
  const pageHost = this.pageHosts.get(index);
196
291
  return (
@@ -200,6 +295,15 @@ export class CollapsiblePagerView extends React.PureComponent<
200
295
  );
201
296
  }
202
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
+
203
307
  private restoreConfiguredScrollElement() {
204
308
  const configured = this.configuredScrollElement;
205
309
  if (!configured) return;
@@ -212,7 +316,10 @@ export class CollapsiblePagerView extends React.PureComponent<
212
316
  style.boxSizing = configured.boxSizing;
213
317
  style.scrollTimelineName = configured.scrollTimelineName;
214
318
  style.scrollTimelineAxis = configured.scrollTimelineAxis;
319
+ style.setProperty(PAGER_HEADER_INSET, configured.pagerHeaderInset);
320
+ style.setProperty(PAGER_STICKY_INSET, configured.pagerStickyInset);
215
321
  configured.element.removeAttribute("data-collapsible-pager-active");
322
+ this.notifyInsetsChanged(configured.element);
216
323
  this.configuredScrollElement = null;
217
324
  }
218
325
 
@@ -223,25 +330,41 @@ export class CollapsiblePagerView extends React.PureComponent<
223
330
  if (this.configuredScrollElement?.element !== element) {
224
331
  this.restoreConfiguredScrollElement();
225
332
  const style = timelineStyle(element);
333
+ const computedPaddingTop =
334
+ element.ownerDocument.defaultView?.getComputedStyle(element)
335
+ .paddingTop || style.paddingTop || "0px";
226
336
  this.configuredScrollElement = {
227
337
  element,
228
338
  paddingTop: style.paddingTop,
339
+ basePaddingTop: computedPaddingTop,
229
340
  boxSizing: style.boxSizing,
230
341
  scrollTimelineName: style.scrollTimelineName,
231
342
  scrollTimelineAxis: style.scrollTimelineAxis,
343
+ pagerHeaderInset: style.getPropertyValue(PAGER_HEADER_INSET),
344
+ pagerStickyInset: style.getPropertyValue(PAGER_STICKY_INSET),
232
345
  };
233
346
  element.addEventListener("scrollend", this.onVerticalScrollSettled);
234
347
  }
235
348
 
236
349
  const style = timelineStyle(element);
237
- style.paddingTop = `${Math.max(
238
- 0,
239
- this.props.headerHeight + this.props.stickyHeaderHeight
240
- )}px`;
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;
241
361
  style.boxSizing = "border-box";
242
362
  style.scrollTimelineName = this.timelineName;
243
363
  style.scrollTimelineAxis = "block";
364
+ style.setProperty(PAGER_HEADER_INSET, headerInsetValue);
365
+ style.setProperty(PAGER_STICKY_INSET, stickyInsetValue);
244
366
  element.setAttribute("data-collapsible-pager-active", "true");
367
+ if (insetsChanged) this.notifyInsetsChanged(element);
245
368
  };
246
369
 
247
370
  private scheduleScrollConfiguration() {
@@ -284,25 +407,24 @@ export class CollapsiblePagerView extends React.PureComponent<
284
407
 
285
408
  private selectPage(selectedPage: number, animated: boolean) {
286
409
  const pages = this.pages();
410
+ const position = pageIndex(selectedPage);
287
411
  if (
288
- selectedPage < 0 ||
289
- selectedPage >= pages.length ||
290
- selectedPage === this.state.selectedPage
412
+ position === null ||
413
+ position < 0 ||
414
+ position >= pages.length ||
415
+ position === this.state.selectedPage
291
416
  ) {
292
417
  return;
293
418
  }
294
419
  this.savePageOffset(this.state.selectedPage);
295
420
 
296
- this.props.onPageScrollStateChanged?.(
297
- this.nativeEvent<OnPageScrollStateChangedEventData>({
298
- pageScrollState: animated ? "settling" : "idle",
299
- })
300
- );
301
- this.setState({ selectedPage, animateTransition: animated }, () => {
421
+ if (animated) this.beginPageTransition();
422
+ else this.finishPageTransition();
423
+ this.setState({ selectedPage: position, animateTransition: animated }, () => {
302
424
  this.scheduleScrollConfiguration();
303
425
  const restore = () => {
304
426
  this.configureActiveScrollElement();
305
- this.restorePageOffset(selectedPage);
427
+ this.restorePageOffset(position);
306
428
  };
307
429
  if (typeof requestAnimationFrame === "function") {
308
430
  requestAnimationFrame(restore);
@@ -310,21 +432,14 @@ export class CollapsiblePagerView extends React.PureComponent<
310
432
  setTimeout(restore, 0);
311
433
  }
312
434
  this.props.onPageSelected?.(
313
- this.nativeEvent<OnPageSelectedEventData>({ position: selectedPage })
435
+ this.nativeEvent<OnPageSelectedEventData>({ position })
314
436
  );
315
437
  this.props.onPageScroll?.(
316
438
  this.nativeEvent<OnPageScrollEventData>({
317
- position: selectedPage,
439
+ position,
318
440
  offset: 0,
319
441
  })
320
442
  );
321
- if (animated) {
322
- this.props.onPageScrollStateChanged?.(
323
- this.nativeEvent<OnPageScrollStateChangedEventData>({
324
- pageScrollState: "idle",
325
- })
326
- );
327
- }
328
443
  this.notifyMountedPagesChanged();
329
444
  this.emitDiagnostics("page-selected");
330
445
  });
@@ -375,42 +490,80 @@ export class CollapsiblePagerView extends React.PureComponent<
375
490
  if (!touch) return;
376
491
  this.touchStartX = touch.pageX;
377
492
  this.touchStartY = touch.pageY;
378
- this.props.onPageScrollStateChanged?.(
379
- this.nativeEvent<OnPageScrollStateChangedEventData>({
380
- pageScrollState: "dragging",
381
- })
382
- );
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
+ }
383
507
  };
384
508
 
385
509
  private onTouchEnd = (event: ReactNative.GestureResponderEvent) => {
386
510
  if (this.touchStartX == null || !this.scrollEnabled) return;
387
511
  const touch = event.nativeEvent.changedTouches[0];
388
- if (!touch) return;
512
+ if (!touch) {
513
+ this.onTouchCancel();
514
+ return;
515
+ }
389
516
  const delta = touch.pageX - this.touchStartX;
390
517
  const verticalDelta = touch.pageY - (this.touchStartY ?? touch.pageY);
391
518
  this.touchStartX = null;
392
519
  this.touchStartY = null;
393
520
  const rtl =
394
521
  this.props.layoutDirection === "rtl" ||
395
- (!this.props.layoutDirection && I18nManager.isRTL);
522
+ ((!this.props.layoutDirection ||
523
+ this.props.layoutDirection === "locale") &&
524
+ I18nManager.isRTL);
396
525
  const direction = Math.abs(delta) >= 40 && Math.abs(delta) > Math.abs(verticalDelta)
397
526
  ? (delta < 0 ? 1 : -1)
398
527
  : 0;
399
528
  const next = this.state.selectedPage + (rtl ? -direction : direction);
400
529
  if (direction === 0 || next < 0 || next >= this.pages().length) {
401
- this.props.onPageScrollStateChanged?.(
402
- this.nativeEvent<OnPageScrollStateChangedEventData>({
403
- pageScrollState: "idle",
404
- })
405
- );
530
+ const wasHorizontal = this.horizontalTouch;
531
+ this.horizontalTouch = false;
532
+ if (wasHorizontal) this.finishPageTransition();
406
533
  return;
407
534
  }
535
+ this.horizontalTouch = false;
408
536
  this.selectPage(next, true);
409
537
  };
410
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
+
411
547
  render() {
412
- const { header, stickyHeader, style, testID, accessibilityLabel } =
413
- this.props;
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;
414
567
  const pages = this.pages();
415
568
  const keys = pages.map(pageKey);
416
569
  const retained = new Set(this.mountedPages(pages.length));
@@ -445,16 +598,15 @@ export class CollapsiblePagerView extends React.PureComponent<
445
598
 
446
599
  return (
447
600
  <View
601
+ {...viewProps}
448
602
  ref={(node) => {
449
- this.root = node as unknown as HTMLElement | null;
603
+ this.root = webElement(node);
450
604
  }}
451
605
  style={[
452
606
  styles.root,
453
607
  { timelineScope: this.timelineName } as never,
454
608
  style,
455
609
  ]}
456
- testID={testID}
457
- accessibilityLabel={accessibilityLabel}
458
610
  >
459
611
  <View
460
612
  style={[
@@ -478,20 +630,19 @@ export class CollapsiblePagerView extends React.PureComponent<
478
630
  {stickyHeader}
479
631
  </View>
480
632
  <View
633
+ ref={this.setTrack}
481
634
  style={trackStyle}
482
635
  onTouchStart={this.onTouchStart}
636
+ onTouchMove={this.onTouchMove}
483
637
  onTouchEnd={this.onTouchEnd}
484
- onTouchCancel={() => {
485
- this.touchStartX = null;
486
- this.touchStartY = null;
487
- }}
638
+ onTouchCancel={this.onTouchCancel}
488
639
  >
489
640
  {pages.map((page, index) => (
490
641
  <View
491
642
  key={keys[index]}
492
643
  ref={(node) => {
493
- if (node) {
494
- const host = node as unknown as HTMLElement;
644
+ const host = webElement(node);
645
+ if (host) {
495
646
  host.inert = index !== this.state.selectedPage;
496
647
  this.pageHosts.set(index, host);
497
648
  } else {
@@ -45,6 +45,8 @@ export interface NativeProps extends ViewProps {
45
45
  layoutDirection?: WithDefault<"ltr" | "rtl", "ltr">;
46
46
  initialPage?: Int32;
47
47
  offscreenPageLimit?: Int32;
48
+ // OneKey patch: Preserve nested pager gesture coordination for collapsible pages.
49
+ nestedScrollEnabled?: WithDefault<boolean, false>;
48
50
  headerHeight?: Int32;
49
51
  stickyHeaderHeight?: Int32;
50
52
  pageKeys?: string;
package/src/index.tsx CHANGED
@@ -1,15 +1,15 @@
1
- import type * as ReactNative from "react-native";
2
- import { PagerView } from "./PagerView";
1
+ import type * as ReactNative from 'react-native';
2
+ import { PagerView } from './PagerView';
3
3
  export default PagerView;
4
- export * from "./usePagerView";
5
- export * from "./CollapsiblePagerView";
4
+ export * from './usePagerView';
5
+ export * from './CollapsiblePagerView';
6
6
 
7
7
  import type {
8
8
  OnPageScrollEventData as PagerViewOnPageScrollEventData,
9
9
  OnPageSelectedEventData as PagerViewOnPageSelectedEventData,
10
10
  OnPageScrollStateChangedEventData as PageScrollStateChangedNativeEventData,
11
11
  NativeProps,
12
- } from "./PagerViewNativeComponent";
12
+ } from './PagerViewNativeComponent';
13
13
 
14
14
  export type {
15
15
  PagerViewOnPageScrollEventData,