@onekeyfe/react-native-pager-view 6.7.0-alpha.435 → 6.7.0-alpha.445

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/README.md +16 -0
  2. package/android/build.gradle +1 -0
  3. package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +135 -0
  4. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +88 -12
  5. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +442 -15
  6. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +11 -1
  7. package/android/src/main/java/com/reactnativepagerview/event/NativeHeaderPressEvent.kt +8 -0
  8. package/docs/SPEC.md +298 -3
  9. package/ios/RNCCollapsiblePagerViewComponentView.mm +834 -20
  10. package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +61 -0
  11. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +22 -0
  12. package/lib/module/CollapsiblePagerView.js +37 -5
  13. package/lib/module/CollapsiblePagerView.web.js +519 -6
  14. package/lib/module/CollapsiblePagerViewNativeComponent.ts +6 -0
  15. package/lib/module/NativeScrollerNativeComponent.ts +0 -1
  16. package/lib/typescript/src/CollapsiblePagerView.d.ts +31 -2
  17. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +44 -1
  18. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +11 -0
  19. package/lib/typescript/src/NativeScroller.d.ts +2 -2
  20. package/package.json +3 -2
  21. package/src/CollapsiblePagerView.tsx +68 -5
  22. package/src/CollapsiblePagerView.web.tsx +464 -5
  23. package/src/CollapsiblePagerViewNativeComponent.ts +6 -0
  24. package/src/NativeScroller.tsx +2 -3
  25. package/src/NativeScrollerNativeComponent.ts +0 -1
@@ -1,6 +1,9 @@
1
1
  import type { ScrollEdgeEffect } from "./ScrollEdgeEffect";
2
2
  import React from "react";
3
- import { I18nManager, Platform, StyleSheet, View } from "react-native";
3
+ import { createNamedIcon } from "@onekeyfe/react-native-image/icon/dom";
4
+ import type { CollapsiblePagerNativeTabBarConfig, CollapsiblePagerToolbarItem, CollapsiblePagerToolbarPress, CollapsiblePagerViewOnNativeTabPressEvent } from "./CollapsiblePagerView";
5
+ export type { CollapsiblePagerNativeTabBarConfig, CollapsiblePagerNativeTabBarItem, CollapsiblePagerNativeTabBarStyle, CollapsiblePagerToolbarItem, CollapsiblePagerToolbarPress } from "./CollapsiblePagerView";
6
+ import { I18nManager, Platform, processColor, StyleSheet, View } from "react-native";
4
7
  import type * as ReactNative from "react-native";
5
8
 
6
9
  import { WebCollapsiblePagerPage } from "./WebCollapsiblePagerPage";
@@ -66,6 +69,10 @@ export interface CollapsiblePagerViewProps
66
69
  | "retainedPages"
67
70
  | "layoutDirection"
68
71
  > {
72
+ nativeTabBar?: CollapsiblePagerNativeTabBarConfig;
73
+ nativeTabPressAnimationEnabled?: boolean;
74
+ onNativeTabPress?: (event: CollapsiblePagerViewOnNativeTabPressEvent) => void;
75
+ onNativeToolbarPress?: (event: ReactNative.NativeSyntheticEvent<CollapsiblePagerToolbarPress>) => void;
69
76
  header: React.ReactNode;
70
77
  stickyHeader: React.ReactNode;
71
78
  headerHeight?: number;
@@ -188,6 +195,10 @@ export class CollapsiblePagerView extends React.PureComponent<
188
195
 
189
196
  private root: HTMLElement | null = null;
190
197
  private track: HTMLElement | null = null;
198
+ private tabProgress: number | null = null;
199
+ private tabProgressFrame: number | null = null;
200
+ private tabProgressGeneration = 0;
201
+ private tabButtons = new Map<string, HTMLButtonElement>();
191
202
  private headerSlot: HTMLElement | null = null;
192
203
  private stickySlot: HTMLElement | null = null;
193
204
  private cssTimelineSupported = false;
@@ -236,6 +247,7 @@ export class CollapsiblePagerView extends React.PureComponent<
236
247
  key: string;
237
248
  }> | null = null;
238
249
  private transitionTimer: ReturnType<typeof setTimeout> | null = null;
250
+ private transitionGeneration = 0;
239
251
  private pageScrollState: OnPageScrollStateChangedEventData["pageScrollState"] =
240
252
  "idle";
241
253
  private configuredScrollElement: ConfiguredScrollElement | null = null;
@@ -290,6 +302,9 @@ export class CollapsiblePagerView extends React.PureComponent<
290
302
  this.scheduleScrollConfiguration();
291
303
  this.syncFallbackHeader();
292
304
  this.notifyMountedPagesChanged();
305
+ this.syncToolbar();
306
+ this.tabProgress = this.state.selectedPage;
307
+ this.syncTabColors();
293
308
  this.emitDiagnostics("mounted");
294
309
  }
295
310
 
@@ -359,6 +374,10 @@ export class CollapsiblePagerView extends React.PureComponent<
359
374
  this.scheduleScrollConfiguration();
360
375
  this.syncFallbackHeader();
361
376
  this.notifyMountedPagesChanged();
377
+ this.syncToolbar();
378
+ if (previousProps.isActive === false && this.props.isActive !== false) this.readTabProgress();
379
+ if (this.props.isActive === false || !this.tabColorWeights()) this.cancelTabProgressFrame();
380
+ this.syncTabColors();
362
381
  }
363
382
 
364
383
  componentWillUnmount() {
@@ -366,6 +385,7 @@ export class CollapsiblePagerView extends React.PureComponent<
366
385
  const routeTarget = this.captureRouteTarget(this.props);
367
386
  if (routeTarget) this.setRouteTarget(routeTarget);
368
387
  this.disposed = true;
388
+ this.setToolbarSlot(null);
369
389
  this.selectionRestoreToken += 1;
370
390
  this.cancelPendingTopFrame();
371
391
  this.savePageOffset(this.state.selectedPage);
@@ -411,6 +431,7 @@ export class CollapsiblePagerView extends React.PureComponent<
411
431
 
412
432
  private observeChrome() {
413
433
  this.chromeObserver = new ResizeObserver(this.scheduleChromeMeasurement);
434
+ if (this.root) this.chromeObserver.observe(this.root);
414
435
  if (this.headerContent) this.chromeObserver.observe(this.headerContent);
415
436
  if (this.stickyContent) this.chromeObserver.observe(this.stickyContent);
416
437
  this.scheduleChromeMeasurement();
@@ -439,6 +460,8 @@ export class CollapsiblePagerView extends React.PureComponent<
439
460
  this.measuredStickyHeight = nextSticky;
440
461
  this.headerMeasured = Boolean(header?.isConnected);
441
462
  this.stickyMeasured = Boolean(sticky?.isConnected);
463
+ this.syncToolbar();
464
+ this.syncTabColors();
442
465
  if (headerChanged || stickyChanged) {
443
466
  this.forceUpdate();
444
467
  this.scheduleScrollConfiguration();
@@ -741,21 +764,32 @@ export class CollapsiblePagerView extends React.PureComponent<
741
764
  }
742
765
 
743
766
  private finishPageTransition = () => {
767
+ this.transitionGeneration += 1;
744
768
  if (this.transitionTimer !== null) {
745
769
  clearTimeout(this.transitionTimer);
746
770
  this.transitionTimer = null;
747
771
  }
772
+ this.readTabProgress();
773
+ this.cancelTabProgressFrame();
774
+ this.syncTabColors();
748
775
  this.emitPageScrollState("idle");
776
+ this.syncToolbar();
749
777
  };
750
778
 
751
779
  private beginPageTransition() {
780
+ this.suspendToolbar();
752
781
  if (this.transitionTimer !== null) clearTimeout(this.transitionTimer);
782
+ const generation = ++this.transitionGeneration;
753
783
  this.emitPageScrollState("settling");
754
- this.transitionTimer = setTimeout(this.finishPageTransition, 320);
784
+ this.transitionTimer = setTimeout(() => {
785
+ if (generation === this.transitionGeneration) this.finishPageTransition();
786
+ }, 320);
755
787
  }
756
788
 
757
789
  private onTrackTransitionEnd = (event: TransitionEvent) => {
758
790
  if (event.target === this.track && event.propertyName === "transform") {
791
+ this.readTabProgress();
792
+ if (this.tabColorWeights() && this.tabProgress !== null && Math.abs(this.tabProgress - this.state.selectedPage) > 0.001) return;
759
793
  this.finishPageTransition();
760
794
  }
761
795
  };
@@ -763,11 +797,120 @@ export class CollapsiblePagerView extends React.PureComponent<
763
797
  private setTrack = (node: unknown) => {
764
798
  const track = webElement(node);
765
799
  if (track === this.track) return;
800
+ this.cancelTabProgressFrame();
766
801
  this.track?.removeEventListener("transitionend", this.onTrackTransitionEnd);
767
802
  this.track = track;
768
803
  this.track?.addEventListener("transitionend", this.onTrackTransitionEnd);
769
804
  };
770
805
 
806
+ private cancelTabProgressFrame() {
807
+ this.tabProgressGeneration += 1;
808
+ if (this.tabProgressFrame !== null) cancelAnimationFrame(this.tabProgressFrame);
809
+ this.tabProgressFrame = null;
810
+ }
811
+
812
+ private readTabProgress() {
813
+ const track = this.track;
814
+ const width = this.root?.getBoundingClientRect().width ?? 0;
815
+ if (!track?.isConnected || width <= 0) return null;
816
+ const transform = track.ownerDocument.defaultView?.getComputedStyle(track).transform;
817
+ const matrix = transform?.match(/^matrix(3d)?\(([^)]+)\)$/);
818
+ const offset = matrix ? Number(matrix[2].split(",")[matrix[1] ? 12 : 4]) : transform === "none" ? 0 : NaN;
819
+ if (!Number.isFinite(offset)) return null;
820
+ this.tabProgress = Math.min(Math.max(0, -offset / width), Math.max(0, this.pages().length - 1));
821
+ return this.tabProgress;
822
+ }
823
+
824
+ private startTabProgressFrames() {
825
+ this.cancelTabProgressFrame();
826
+ if (!this.tabColorWeights() || typeof requestAnimationFrame !== "function") return;
827
+ const track = this.track;
828
+ const root = this.root;
829
+ const width = root?.getBoundingClientRect().width ?? 0;
830
+ if (!track?.isConnected || !root?.isConnected || width <= 0 || this.props.isActive === false) return;
831
+ const generation = this.tabProgressGeneration;
832
+ const selection = this.selectionRestoreToken;
833
+ const keys = JSON.stringify(this.pages().map(pageKey));
834
+ const owner = this.pageScrollElement(this.state.selectedPage);
835
+ const sample = () => {
836
+ if (generation !== this.tabProgressGeneration || this.disposed) return;
837
+ this.tabProgressFrame = null;
838
+ if (this.track !== track || this.root !== root || !track.isConnected || !root.isConnected ||
839
+ this.props.isActive === false || selection !== this.selectionRestoreToken ||
840
+ keys !== JSON.stringify(this.pages().map(pageKey)) || owner !== this.pageScrollElement(this.state.selectedPage) ||
841
+ width !== root.getBoundingClientRect().width || !this.tabColorWeights()) {
842
+ this.cancelTabProgressFrame();
843
+ this.readTabProgress();
844
+ this.syncTabColors();
845
+ if (this.transitionTimer !== null) this.startTabProgressFrames();
846
+ return;
847
+ }
848
+ this.readTabProgress();
849
+ this.syncTabColors();
850
+ if (this.transitionTimer !== null) this.tabProgressFrame = requestAnimationFrame(sample);
851
+ };
852
+ sample();
853
+ }
854
+
855
+ private tabColorWeights() {
856
+ const config = this.props.nativeTabBar;
857
+ if (!config?.controlled || config.style?.variant !== "text" || this.tabProgress === null) return null;
858
+ const keys: string[] = [];
859
+ React.Children.forEach(this.props.children, child => {
860
+ if (child !== null && child !== undefined && typeof child !== "boolean") keys.push(pageKey(child, keys.length));
861
+ });
862
+ const itemKeys = config.items.map(item => item.key);
863
+ if (!keys.length || !keys.includes(config.selectedKey ?? "") || new Set(keys).size !== keys.length ||
864
+ new Set(itemKeys).size !== itemKeys.length || keys.some(key => !itemKeys.includes(key))) return null;
865
+ const progress = Math.min(Math.max(0, this.tabProgress), keys.length - 1);
866
+ const source = Math.floor(progress);
867
+ const destination = Math.min(source + 1, keys.length - 1);
868
+ const fraction = progress - source;
869
+ return new Map(keys.map((key, index) => [key, index === source ? 1 - fraction : index === destination ? fraction : 0]));
870
+ }
871
+
872
+ private tabFontOwnerKey(weights: Map<string, number> | null) {
873
+ const config = this.props.nativeTabBar;
874
+ const semantic = config?.controlled ? config.selectedKey : config?.items[this.state.selectedPage]?.key;
875
+ if (!weights || this.tabProgress === null) return semantic;
876
+ const keys = Array.from(weights.keys());
877
+ const progress = Math.min(Math.max(0, this.tabProgress), keys.length - 1);
878
+ return keys[Math.floor(progress + 0.5)];
879
+ }
880
+
881
+ private tabColor(selected: boolean, weight: number | undefined) {
882
+ const style = this.props.nativeTabBar?.style;
883
+ const active = style?.activeTextColor ?? "#000000";
884
+ const inactive = style?.inactiveTextColor ?? "#666666";
885
+ if (weight === undefined) return String(selected ? active : inactive);
886
+ if (weight <= 0) return String(inactive);
887
+ if (weight >= 1) return String(active);
888
+ const first = processColor(inactive);
889
+ const last = processColor(active);
890
+ if (typeof first !== "number" || typeof last !== "number") return String(selected ? active : inactive);
891
+ const channel = (shift: number) => ((first >>> shift) & 255) * (1 - weight) + ((last >>> shift) & 255) * weight;
892
+ return `rgba(${Math.round(channel(16))}, ${Math.round(channel(8))}, ${Math.round(channel(0))}, ${(channel(24) / 255).toFixed(4)})`;
893
+ }
894
+
895
+ private syncTabColors() {
896
+ const weights = this.tabColorWeights();
897
+ const config = this.props.nativeTabBar;
898
+ if (!config) return;
899
+ const selectedKey = config.controlled ? config.selectedKey : config.items[this.state.selectedPage]?.key;
900
+ const fontOwnerKey = this.tabFontOwnerKey(weights);
901
+ for (const [key, button] of this.tabButtons) {
902
+ const fontSelected = key === fontOwnerKey;
903
+ button.style.color = this.tabColor(key === selectedKey, weights ? weights.get(key) ?? 0 : undefined);
904
+ button.style.fontWeight = config.style?.variant === "text" && fontSelected ? "600" : "500";
905
+ const fontFamily = (fontSelected ? config.style?.activeFontFamily ?? config.style?.fontFamily : config.style?.fontFamily) ?? "";
906
+ if (config.controlled && config.style?.variant === "text") {
907
+ button.style.fontFamily = config.style?.fontFamily ?? "";
908
+ const label = button.querySelector<HTMLElement>(".ok-pager-tab-label");
909
+ if (label) label.style.fontFamily = fontFamily;
910
+ } else button.style.fontFamily = fontFamily;
911
+ }
912
+ }
913
+
771
914
  private pageScrollElement(index: number) {
772
915
  const pageHost = this.pageHosts.get(index);
773
916
  return (
@@ -790,7 +933,10 @@ export class CollapsiblePagerView extends React.PureComponent<
790
933
  this.root?.removeEventListener("touchend", this.onOverlayTouchEnd);
791
934
  this.root?.removeEventListener("touchcancel", this.onOverlayTouchEnd);
792
935
  this.overlayTouch = null;
936
+ if (this.root) this.chromeObserver?.unobserve(this.root);
793
937
  this.root = root;
938
+ if (root) this.chromeObserver?.observe(root);
939
+ else this.suspendToolbar();
794
940
  root?.addEventListener(
795
941
  "ok-collapsible-pager-scroll-key-changed",
796
942
  this.onScrollOwnerIdentityChanged
@@ -1191,10 +1337,14 @@ export class CollapsiblePagerView extends React.PureComponent<
1191
1337
  this.savePageOffset(this.state.selectedPage);
1192
1338
 
1193
1339
  if (animated) this.beginPageTransition();
1194
- else this.finishPageTransition();
1340
+ else {
1341
+ this.finishPageTransition();
1342
+ this.tabProgress = position;
1343
+ }
1195
1344
  this.setState(
1196
1345
  { selectedPage: position, animateTransition: animated },
1197
1346
  () => {
1347
+ if (animated) this.startTabProgressFrames();
1198
1348
  this.scheduleScrollConfiguration();
1199
1349
  const restore = () => {
1200
1350
  if (
@@ -1294,6 +1444,7 @@ export class CollapsiblePagerView extends React.PureComponent<
1294
1444
  this.cancelPendingTopFrame();
1295
1445
  this.horizontalTouch = true;
1296
1446
  this.emitPageScrollState("dragging");
1447
+ this.suspendToolbar();
1297
1448
  }
1298
1449
  };
1299
1450
 
@@ -1338,9 +1489,317 @@ export class CollapsiblePagerView extends React.PureComponent<
1338
1489
  if (wasHorizontal) this.finishPageTransition();
1339
1490
  };
1340
1491
 
1492
+ private toolbarSlot: HTMLDivElement | null = null;
1493
+ private toolbarButton: HTMLButtonElement | null = null;
1494
+ private toolbarOwnerKey: string | undefined;
1495
+ private toolbarOwnerEntry: CollapsiblePagerToolbarItem | undefined;
1496
+ private toolbarIconIdentity = "";
1497
+ private toolbarLoadedIcon: SVGSVGElement | null = null;
1498
+ private toolbarPendingIdentity = "";
1499
+ private toolbarReadyIcon: SVGSVGElement | null = null;
1500
+ private toolbarFailedIdentity = "";
1501
+ private toolbarAnimation: Animation | null = null;
1502
+ private toolbarPresence = false;
1503
+ private toolbarPresenceInterrupted = false;
1504
+ private toolbarImage: HTMLImageElement | null = null;
1505
+ private toolbarGeneration = 0;
1506
+ private toolbarTouchKey: string | null | undefined;
1507
+
1508
+ private ownsSettledToolbar() {
1509
+ return this.props.nativeTabBar?.controlled === true && this.props.nativeTabBar.toolbar !== undefined;
1510
+ }
1511
+
1512
+ private toolbarCanCommit() {
1513
+ if (!this.ownsSettledToolbar()) return true;
1514
+ if (this.disposed || this.props.isActive === false || this.horizontalTouch ||
1515
+ this.transitionTimer !== null || this.pageScrollState !== "idle" || !this.root?.isConnected) return false;
1516
+ const actual = this.readTabProgress();
1517
+ if (actual === null || Math.abs(actual - this.state.selectedPage) > 0.001) return false;
1518
+ const config = this.props.nativeTabBar!;
1519
+ const keys: string[] = [];
1520
+ React.Children.forEach(this.props.children, child => {
1521
+ if (child !== null && child !== undefined && typeof child !== "boolean") keys.push(pageKey(child, keys.length));
1522
+ });
1523
+ const itemKeys = config.items.map(item => item.key);
1524
+ const valid = keys.length > 0 && new Set(keys).size === keys.length &&
1525
+ new Set(itemKeys).size === itemKeys.length && keys.every(key => itemKeys.includes(key));
1526
+ return !valid || !keys.includes(config.selectedKey ?? "") || keys[this.state.selectedPage] === config.selectedKey;
1527
+ }
1528
+
1529
+ private cancelToolbarPending() {
1530
+ if (this.toolbarPendingIdentity || this.toolbarImage || this.toolbarReadyIcon) this.toolbarGeneration += 1;
1531
+ if (this.toolbarImage) this.toolbarImage.onload = this.toolbarImage.onerror = null;
1532
+ this.toolbarImage = null;
1533
+ this.toolbarPendingIdentity = "";
1534
+ this.toolbarReadyIcon = null;
1535
+ }
1536
+
1537
+ private setToolbarInteractive(enabled: boolean) {
1538
+ const button = this.toolbarButton;
1539
+ if (!button) return;
1540
+ if (!enabled) {
1541
+ if (button.ownerDocument.activeElement === button) button.blur();
1542
+ if (this.toolbarTouchKey !== undefined) this.toolbarTouchKey = null;
1543
+ }
1544
+ button.disabled = !enabled;
1545
+ button.style.pointerEvents = enabled ? "auto" : "none";
1546
+ button.tabIndex = enabled ? 0 : -1;
1547
+ button.setAttribute("aria-hidden", String(!enabled));
1548
+ }
1549
+
1550
+ private suspendToolbar() {
1551
+ if (!this.ownsSettledToolbar()) return;
1552
+ this.setToolbarInteractive(false);
1553
+ this.cancelToolbarPending();
1554
+ if (this.toolbarAnimation && this.toolbarButton) {
1555
+ const opacity = this.toolbarButton.ownerDocument.defaultView?.getComputedStyle(this.toolbarButton).opacity;
1556
+ this.toolbarAnimation.cancel();
1557
+ this.toolbarAnimation = null;
1558
+ this.toolbarButton.style.opacity = opacity || this.toolbarButton.style.opacity;
1559
+ this.toolbarPresenceInterrupted = true;
1560
+ }
1561
+ }
1562
+
1563
+ private setToolbarSlot = (slot: HTMLDivElement | null) => {
1564
+ if (this.toolbarSlot === slot) return;
1565
+ this.toolbarAnimation?.cancel();
1566
+ this.toolbarAnimation = null;
1567
+ this.cancelToolbarPending();
1568
+ this.toolbarGeneration += 1;
1569
+ this.toolbarButton?.remove();
1570
+ this.toolbarButton = null;
1571
+ this.toolbarOwnerKey = undefined;
1572
+ this.toolbarOwnerEntry = undefined;
1573
+ this.toolbarIconIdentity = "";
1574
+ this.toolbarLoadedIcon = null;
1575
+ this.toolbarFailedIdentity = "";
1576
+ this.toolbarPresence = false;
1577
+ this.toolbarPresenceInterrupted = false;
1578
+ this.toolbarTouchKey = null;
1579
+ this.toolbarSlot = slot;
1580
+ if (slot) this.syncToolbar();
1581
+ };
1582
+
1583
+ private animateToolbar(visible: boolean) {
1584
+ const button = this.toolbarButton;
1585
+ if (!button || (this.toolbarPresence === visible && !this.toolbarPresenceInterrupted)) return;
1586
+ this.toolbarPresenceInterrupted = false;
1587
+ this.toolbarPresence = visible;
1588
+ const current = button.ownerDocument.defaultView?.getComputedStyle(button).opacity ?? button.style.opacity;
1589
+ this.toolbarAnimation?.cancel();
1590
+ this.toolbarAnimation = null;
1591
+ button.style.opacity = visible ? "1" : "0";
1592
+ const finish = () => {
1593
+ button.hidden = !visible;
1594
+ if (!visible && !this.toolbarOwnerKey && this.toolbarButton === button) {
1595
+ button.remove();
1596
+ this.toolbarButton = null;
1597
+ }
1598
+ };
1599
+ if (typeof button.animate !== "function") { finish(); return; }
1600
+ button.hidden = false;
1601
+ const animation = button.animate([{ opacity: current || "0" }, { opacity: visible ? "1" : "0" }], { duration: 100, fill: "forwards" });
1602
+ this.toolbarAnimation = animation;
1603
+ animation.onfinish = () => {
1604
+ if (this.toolbarAnimation !== animation || this.toolbarButton !== button || this.toolbarPresence !== visible) return;
1605
+ animation.cancel();
1606
+ this.toolbarAnimation = null;
1607
+ finish();
1608
+ };
1609
+ }
1610
+
1611
+ private hideToolbar(failed: boolean) {
1612
+ this.setToolbarInteractive(false);
1613
+ this.toolbarOwnerKey = undefined;
1614
+ this.toolbarOwnerEntry = undefined;
1615
+ this.toolbarButton?.removeAttribute("data-testid");
1616
+ this.toolbarButton?.removeAttribute("aria-label");
1617
+ if (failed) this.toolbarButton?.replaceChildren();
1618
+ this.animateToolbar(false);
1619
+ }
1620
+
1621
+ private commitToolbar(key: string, entry: CollapsiblePagerToolbarItem, icon: SVGSVGElement, identity: string) {
1622
+ const button = this.toolbarButton;
1623
+ if (!button || !this.toolbarCanCommit()) return;
1624
+ if (this.toolbarOwnerKey !== key && this.toolbarTouchKey !== undefined) this.toolbarTouchKey = null;
1625
+ const size = Math.min(128, Math.max(1, entry.size ?? 38));
1626
+ const iconSize = Math.min(size, Math.max(1, entry.iconSize ?? 24));
1627
+ const contentWidth = Math.min(size, Math.max(0, entry.contentWidth ?? 24));
1628
+ const trailingInset = Math.min(128, Math.max(0, entry.trailingInset ?? 20));
1629
+ if (button.firstChild !== icon) button.replaceChildren(icon);
1630
+ Object.assign(button.style, { position: "absolute", width: `${size}px`, height: `${size}px`, borderRadius: "50%", top: "50%", transform: "translateY(-50%)",
1631
+ insetInlineEnd: `${trailingInset - (size - contentWidth) / 2}px`, padding: `${(size - iconSize) / 2}px`, color: entry.iconColor ?? "#000000" });
1632
+ button.style.setProperty("--pager-toolbar-base", entry.selected ? entry.selectedBackgroundColor ?? "transparent" : "transparent");
1633
+ button.style.setProperty("--pager-toolbar-hover", entry.hoverBackgroundColor ?? "transparent");
1634
+ button.style.setProperty("--pager-toolbar-pressed", entry.pressedBackgroundColor ?? "transparent");
1635
+ button.setAttribute("aria-label", entry.accessibilityLabel ?? "");
1636
+ if (entry.testID) button.setAttribute("data-testid", entry.testID); else button.removeAttribute("data-testid");
1637
+ this.toolbarOwnerKey = key;
1638
+ this.toolbarOwnerEntry = { ...entry };
1639
+ this.toolbarIconIdentity = identity;
1640
+ this.toolbarLoadedIcon = icon;
1641
+ this.toolbarPendingIdentity = "";
1642
+ this.toolbarReadyIcon = null;
1643
+ this.toolbarImage = null;
1644
+ this.animateToolbar(true);
1645
+ this.setToolbarInteractive(true);
1646
+ }
1647
+
1648
+ private syncToolbar() {
1649
+ const slot = this.toolbarSlot;
1650
+ if (!slot) return;
1651
+ if (!this.toolbarCanCommit()) { this.suspendToolbar(); return; }
1652
+ const config = this.props.nativeTabBar;
1653
+ const key = config?.controlled ? config.selectedKey : config?.items[this.state.selectedPage]?.key;
1654
+ const entry = key ? config?.toolbar?.[key] : undefined;
1655
+ if (!entry?.iconName || !key) {
1656
+ this.cancelToolbarPending();
1657
+ this.hideToolbar(false);
1658
+ return;
1659
+ }
1660
+ let button = this.toolbarButton;
1661
+ if (!button) {
1662
+ button = slot.ownerDocument.createElement("button");
1663
+ button.type = "button";
1664
+ button.className = "ok-pager-toolbar";
1665
+ button.style.opacity = "0";
1666
+ button.hidden = true;
1667
+ button.disabled = true;
1668
+ button.addEventListener("pointerdown", () => { this.toolbarTouchKey = button!.disabled ? null : this.toolbarOwnerKey ?? null; });
1669
+ button.addEventListener("pointercancel", () => { this.toolbarTouchKey = null; });
1670
+ button.onclick = event => {
1671
+ if (this.toolbarButton !== event.currentTarget) return;
1672
+ const pointer = event.detail !== 0;
1673
+ const touched = this.toolbarTouchKey;
1674
+ if (pointer) this.toolbarTouchKey = undefined;
1675
+ const current = this.props.nativeTabBar;
1676
+ const semantic = current?.controlled ? current.selectedKey : current?.items[this.state.selectedPage]?.key;
1677
+ const owner = this.toolbarOwnerKey;
1678
+ if (button!.disabled || !this.toolbarCanCommit() || !owner || owner !== semantic ||
1679
+ !this.toolbarOwnerEntry || (pointer && touched !== owner)) return;
1680
+ const rect = button!.getBoundingClientRect();
1681
+ this.props.onNativeToolbarPress?.({ nativeEvent: { key: owner,
1682
+ anchorRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } } } as ReactNative.NativeSyntheticEvent<CollapsiblePagerToolbarPress>);
1683
+ };
1684
+ slot.appendChild(button);
1685
+ this.toolbarButton = button;
1686
+ this.toolbarPresence = false;
1687
+ }
1688
+ const size = Math.min(128, Math.max(1, entry.size ?? 38));
1689
+ const iconSize = Math.min(size, Math.max(1, entry.iconSize ?? 24));
1690
+ const identity = `${entry.iconName}|${entry.iconColor ?? "#000000"}|${iconSize}`;
1691
+ if (identity === this.toolbarIconIdentity && this.toolbarLoadedIcon) {
1692
+ this.cancelToolbarPending();
1693
+ this.commitToolbar(key, entry, this.toolbarLoadedIcon, identity);
1694
+ return;
1695
+ }
1696
+ if (identity === this.toolbarPendingIdentity) {
1697
+ if (this.toolbarReadyIcon) this.commitToolbar(key, entry, this.toolbarReadyIcon, identity);
1698
+ return;
1699
+ }
1700
+ this.cancelToolbarPending();
1701
+ this.setToolbarInteractive(false);
1702
+ this.toolbarPendingIdentity = identity;
1703
+ if (identity === this.toolbarFailedIdentity) { this.hideToolbar(true); return; }
1704
+ const generation = ++this.toolbarGeneration;
1705
+ const icon = createNamedIcon(button.ownerDocument, entry.iconName);
1706
+ icon.setAttribute("width", String(iconSize)); icon.setAttribute("height", String(iconSize));
1707
+ const use = icon.querySelector("use");
1708
+ const fail = () => {
1709
+ if (this.toolbarGeneration !== generation || this.toolbarPendingIdentity !== identity) return;
1710
+ this.toolbarImage = null;
1711
+ this.toolbarFailedIdentity = identity;
1712
+ this.hideToolbar(true);
1713
+ };
1714
+ if (!use) { fail(); return; }
1715
+ const ready = () => {
1716
+ if (this.disposed || this.toolbarGeneration !== generation || this.toolbarPendingIdentity !== identity) return;
1717
+ this.toolbarImage = null;
1718
+ this.toolbarReadyIcon = icon;
1719
+ this.syncToolbar();
1720
+ };
1721
+ const href = use.getAttribute("href") ?? use.getAttribute("xlink:href") ?? "";
1722
+ const view = button.ownerDocument.defaultView;
1723
+ if (view?.Image && href.split("#")[0]) {
1724
+ const image = new view.Image();
1725
+ this.toolbarImage = image;
1726
+ image.onload = ready;
1727
+ image.onerror = fail;
1728
+ image.src = href.split("#")[0];
1729
+ } else ready();
1730
+ }
1731
+
1732
+ private renderNativeTabBar() {
1733
+ const config = this.props.nativeTabBar;
1734
+ if (!config?.items.length || (config.style?.variant !== "text" && config.controlled !== true && config.toolbar === undefined)) return null;
1735
+ const style = config.style;
1736
+ const variant = style?.variant ?? "underline";
1737
+ const selectedKey = config.controlled ? config.selectedKey : config.items[this.state.selectedPage]?.key;
1738
+ const toolbarEntries = config.toolbar === undefined ? undefined : Object.values(config.toolbar);
1739
+ const reservedWidth = toolbarEntries === undefined ? 0 : toolbarEntries.length === 0 ? 44 : Math.max(...toolbarEntries.map(entry => {
1740
+ const size = Math.min(128, Math.max(1, entry.size ?? 38));
1741
+ return Math.min(size, Math.max(0, entry.contentWidth ?? 24)) + Math.min(128, Math.max(0, entry.trailingInset ?? 20));
1742
+ }));
1743
+ const segmented = variant === "segmented";
1744
+ const reserveTextFaces = config.controlled && variant === "text";
1745
+ const colorWeights = this.tabColorWeights();
1746
+ const fontOwnerKey = this.tabFontOwnerKey(colorWeights);
1747
+ return React.createElement("div", {
1748
+ className: "ok-pager-tab-bar",
1749
+ dir: this.props.layoutDirection === "rtl" || ((this.props.layoutDirection ?? "locale") === "locale" && I18nManager.isRTL) ? "rtl" : "ltr",
1750
+ style: { display: "flex", alignItems: "center", position: "relative", height: style?.height ?? 44,
1751
+ backgroundColor: String(style?.backgroundColor ?? "transparent"), minWidth: 0 },
1752
+ },
1753
+ React.createElement("style", null, `.ok-pager-tab{box-sizing:border-box;appearance:none;border:0;background:transparent;cursor:default;white-space:nowrap;flex-shrink:0;line-height:normal}.ok-pager-toolbar{box-sizing:border-box;appearance:none;border:1px solid transparent;cursor:default;display:flex;align-items:center;justify-content:center;background:var(--pager-toolbar-base)}.ok-pager-toolbar:hover:enabled{background:var(--pager-toolbar-hover)}.ok-pager-toolbar:active:enabled{background:var(--pager-toolbar-pressed)}`),
1754
+ React.createElement("div", { role: "tablist", style: { display: "flex", overflowX: segmented ? "hidden" : "auto", flex: 1,
1755
+ padding: `${style?.contentPaddingVertical ?? 0}px ${style?.contentPaddingHorizontal ?? 20}px`, gap: style?.itemSpacing ?? 8 } },
1756
+ ...config.items.map((item, position) => {
1757
+ const selected = item.key === selectedKey;
1758
+ const fontSelected = item.key === fontOwnerKey;
1759
+ return React.createElement("button", {
1760
+ key: item.key, role: "tab", type: "button", className: "ok-pager-tab", "aria-selected": selected,
1761
+ ref: (button: HTMLButtonElement | null) => { if (button) this.tabButtons.set(item.key, button); else this.tabButtons.delete(item.key); },
1762
+ "aria-label": item.accessibilityLabel ?? item.title, "data-testid": item.testID,
1763
+ style: { padding: variant === "text" ? "6px 10px" : "6px 8px", minWidth: 44, height: (style?.height ?? 44) - 2 * (style?.contentPaddingVertical ?? 0),
1764
+ fontSize: style?.fontSize ?? (variant === "text" ? 18 : 16),
1765
+ fontWeight: variant === "text" && fontSelected ? 600 : 500,
1766
+ fontFamily: reserveTextFaces ? style?.fontFamily : fontSelected ? style?.activeFontFamily ?? style?.fontFamily : style?.fontFamily,
1767
+ display: reserveTextFaces ? "inline-grid" : undefined,
1768
+ gridTemplateAreas: reserveTextFaces ? '"label"' : undefined,
1769
+ alignItems: reserveTextFaces ? "center" : undefined,
1770
+ color: this.tabColor(selected, colorWeights ? colorWeights.get(item.key) ?? 0 : undefined),
1771
+ flex: segmented ? 1 : undefined, borderRadius: segmented ? 999 : 0,
1772
+ backgroundColor: segmented && selected ? String(style?.indicatorColor ?? "transparent") : "transparent",
1773
+ borderBottom: variant === "underline" && selected ? `${style?.indicatorHeight ?? 2}px solid ${String(style?.indicatorColor ?? "#000000")}` : undefined },
1774
+ onClick: () => {
1775
+ if (!config.controlled) {
1776
+ const animated = this.props.nativeTabPressAnimationEnabled ?? Math.abs(position - this.state.selectedPage) <= 1;
1777
+ if (animated) this.setPage(position); else this.setPageWithoutAnimation(position);
1778
+ }
1779
+ this.props.onNativeTabPress?.({ nativeEvent: { position, key: item.key } } as CollapsiblePagerViewOnNativeTabPressEvent);
1780
+ },
1781
+ }, reserveTextFaces ? [
1782
+ React.createElement("span", { key: "label", className: "ok-pager-tab-label", style: {
1783
+ gridArea: "label", fontFamily: fontSelected ? style?.activeFontFamily ?? style?.fontFamily : style?.fontFamily,
1784
+ } }, item.title),
1785
+ ...[false, true].map(active => React.createElement("span", {
1786
+ key: active ? "active-width" : "normal-width", "aria-hidden": true,
1787
+ style: { gridArea: "label", visibility: "hidden", pointerEvents: "none", userSelect: "none",
1788
+ fontWeight: active ? 600 : 500,
1789
+ fontFamily: active ? style?.activeFontFamily ?? style?.fontFamily : style?.fontFamily },
1790
+ }, item.title)),
1791
+ ] : item.title);
1792
+ })),
1793
+ config.toolbar !== undefined ? React.createElement("div", { ref: this.setToolbarSlot, className: "ok-pager-toolbar-slot", style: { flexShrink: 0, width: reservedWidth, height: "100%", position: "relative" } }) : null);
1794
+ }
1795
+
1341
1796
  render() {
1342
1797
  const {
1343
1798
  children: _children,
1799
+ nativeTabBar: _nativeTabBar,
1800
+ nativeTabPressAnimationEnabled: _nativeTabPressAnimationEnabled,
1801
+ onNativeTabPress: _onNativeTabPress,
1802
+ onNativeToolbarPress: _onNativeToolbarPress,
1344
1803
  header,
1345
1804
  stickyHeader,
1346
1805
  topOverlay: _topOverlay,
@@ -1436,8 +1895,8 @@ export class CollapsiblePagerView extends React.PureComponent<
1436
1895
  collapseAnimation,
1437
1896
  ]}
1438
1897
  >
1439
- {this.hasExplicitStickyHeaderHeight ? stickyHeader : (
1440
- <View ref={this.setStickyContent}>{stickyHeader}</View>
1898
+ {this.hasExplicitStickyHeaderHeight ? <>{this.renderNativeTabBar()}{stickyHeader}</> : (
1899
+ <View ref={this.setStickyContent}>{this.renderNativeTabBar()}{stickyHeader}</View>
1441
1900
  )}
1442
1901
  </View>
1443
1902
  <View
@@ -38,6 +38,10 @@ export type OnNativeTabPressEventData = Readonly<{
38
38
  position: CodegenTypes.Int32;
39
39
  key: string;
40
40
  }>;
41
+ export type OnNativeToolbarPressEventData = Readonly<{
42
+ key: string;
43
+ anchorRect: Readonly<{ x: CodegenTypes.Double; y: CodegenTypes.Double; width: CodegenTypes.Double; height: CodegenTypes.Double }>;
44
+ }>;
41
45
  export type OnScrollEdgeCapabilityEventData = Readonly<{ available: boolean }>;
42
46
 
43
47
  /**
@@ -60,6 +64,7 @@ export interface NativeProps extends ViewProps {
60
64
  externalTopOverlayTag?: CodegenTypes.WithDefault<CodegenTypes.Int32, 0>;
61
65
  pageKeys?: string;
62
66
  retainedPages?: string;
67
+ nativeTabBarConfig?: string;
63
68
  nativeTabBarItems?: string;
64
69
  nativeTabBarHeight?: CodegenTypes.Double;
65
70
  nativeTabBarVariant?: string;
@@ -81,6 +86,7 @@ export interface NativeProps extends ViewProps {
81
86
  onPageSelected?: CodegenTypes.DirectEventHandler<OnPageSelectedEventData>;
82
87
  onPageScrollStateChanged?: CodegenTypes.DirectEventHandler<OnPageScrollStateChangedEventData>;
83
88
  onCollapsibleStateChanged?: CodegenTypes.DirectEventHandler<OnCollapsibleStateChangedEventData>;
89
+ onNativeToolbarPress?: CodegenTypes.DirectEventHandler<OnNativeToolbarPressEventData>;
84
90
  onNativeTabPress?: CodegenTypes.DirectEventHandler<OnNativeTabPressEventData>;
85
91
  onNativeSubHeaderPress?: CodegenTypes.DirectEventHandler<OnNativeTabPressEventData>;
86
92
  onScrollEdgeCapability?: CodegenTypes.DirectEventHandler<OnScrollEdgeCapabilityEventData>;
@@ -7,7 +7,6 @@ import {
7
7
  TextInput,
8
8
  View,
9
9
  type GestureResponderEvent,
10
- type HostInstance,
11
10
  type RefreshControlProps,
12
11
  type ScrollView,
13
12
  type ScrollViewProps,
@@ -62,7 +61,7 @@ type HostRef = React.ElementRef<typeof NativeScrollerHost>;
62
61
 
63
62
  export interface NativeScrollerRef
64
63
  extends Pick<
65
- HostInstance,
64
+ HostRef,
66
65
  | "measure"
67
66
  | "measureInWindow"
68
67
  | "measureLayout"
@@ -166,7 +165,7 @@ export const NativeScroller = React.forwardRef<
166
165
 
167
166
  React.useImperativeHandle(
168
167
  forwardedRef,
169
- () => ({
168
+ (): NativeScrollerRef => ({
170
169
  scrollTo(options, x, animated) {
171
170
  const host = hostRef.current;
172
171
  if (!host) return;
@@ -8,7 +8,6 @@ import {
8
8
  type ViewProps,
9
9
  } from "react-native";
10
10
 
11
-
12
11
  export type NativeScrollerScrollEvent = Readonly<{
13
12
  contentInset: Readonly<{
14
13
  top: CodegenTypes.Double;