react-panel-layout 0.6.0 → 0.6.1

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 (216) hide show
  1. package/dist/{FloatingPanelFrame-SgYLc6Ud.js → FloatingPanelFrame-3eU9AwPo.js} +2 -2
  2. package/dist/{FloatingPanelFrame-SgYLc6Ud.js.map → FloatingPanelFrame-3eU9AwPo.js.map} +1 -1
  3. package/dist/FloatingWindow-CUXnEtrb.js +827 -0
  4. package/dist/FloatingWindow-CUXnEtrb.js.map +1 -0
  5. package/dist/FloatingWindow-DMwyK0eK.cjs +2 -0
  6. package/dist/FloatingWindow-DMwyK0eK.cjs.map +1 -0
  7. package/dist/GridLayout-DKTg_N61.cjs +2 -0
  8. package/dist/{GridLayout-B4VRsC0r.cjs.map → GridLayout-DKTg_N61.cjs.map} +1 -1
  9. package/dist/{GridLayout-BltqeCPK.js → GridLayout-UWNxXw77.js} +34 -35
  10. package/dist/{GridLayout-BltqeCPK.js.map → GridLayout-UWNxXw77.js.map} +1 -1
  11. package/dist/{HorizontalDivider-WF1k_qND.js → HorizontalDivider-DdxzfV0l.js} +3 -3
  12. package/dist/{HorizontalDivider-WF1k_qND.js.map → HorizontalDivider-DdxzfV0l.js.map} +1 -1
  13. package/dist/{HorizontalDivider-B5Z-KZLk.cjs → HorizontalDivider-_pgV4Mcv.cjs} +2 -2
  14. package/dist/{HorizontalDivider-B5Z-KZLk.cjs.map → HorizontalDivider-_pgV4Mcv.cjs.map} +1 -1
  15. package/dist/{PanelSystem-Dr1TBhxM.js → PanelSystem-BqUzNtf2.js} +5 -5
  16. package/dist/{PanelSystem-Dr1TBhxM.js.map → PanelSystem-BqUzNtf2.js.map} +1 -1
  17. package/dist/{PanelSystem-Bs8bQwQF.cjs → PanelSystem-D603LKKv.cjs} +2 -2
  18. package/dist/{PanelSystem-Bs8bQwQF.cjs.map → PanelSystem-D603LKKv.cjs.map} +1 -1
  19. package/dist/ResizeHandle-CBcAS918.cjs +2 -0
  20. package/dist/{ResizeHandle-CScipO5l.cjs.map → ResizeHandle-CBcAS918.cjs.map} +1 -1
  21. package/dist/{ResizeHandle-CdA_JYfN.js → ResizeHandle-CXjc1meV.js} +28 -29
  22. package/dist/{ResizeHandle-CdA_JYfN.js.map → ResizeHandle-CXjc1meV.js.map} +1 -1
  23. package/dist/SwipePivotTabBar-DWrCuwEI.js +411 -0
  24. package/dist/SwipePivotTabBar-DWrCuwEI.js.map +1 -0
  25. package/dist/SwipePivotTabBar-fjjXkpj7.cjs +2 -0
  26. package/dist/SwipePivotTabBar-fjjXkpj7.cjs.map +1 -0
  27. package/dist/components/gesture/SwipeSafeZone.d.ts +40 -0
  28. package/dist/components/window/Drawer.d.ts +3 -1
  29. package/dist/components/window/DrawerLayers.d.ts +1 -1
  30. package/dist/components/window/drawerStyles.d.ts +69 -0
  31. package/dist/components/window/drawerSwipeConfig.d.ts +29 -0
  32. package/dist/components/window/useDrawerSwipeTransform.d.ts +23 -0
  33. package/dist/config.cjs +1 -1
  34. package/dist/config.js +3 -3
  35. package/dist/constants/styles.d.ts +17 -0
  36. package/dist/dialog/index.d.ts +69 -0
  37. package/dist/floating.js +1 -1
  38. package/dist/grid.cjs +1 -1
  39. package/dist/grid.js +2 -2
  40. package/dist/hooks/gesture/testing/createGestureSimulator.d.ts +7 -0
  41. package/dist/hooks/gesture/types.d.ts +48 -5
  42. package/dist/hooks/gesture/utils.d.ts +19 -0
  43. package/dist/hooks/useAnimationFrame.d.ts +2 -0
  44. package/dist/hooks/useOperationContinuity.d.ts +64 -0
  45. package/dist/hooks/useResizeObserver.d.ts +33 -1
  46. package/dist/hooks/useSharedElementTransition.d.ts +112 -0
  47. package/dist/hooks/useSwipeContentTransform.d.ts +9 -2
  48. package/dist/index.cjs +1 -1
  49. package/dist/index.js +7 -7
  50. package/dist/modules/dialog/AlertDialog.d.ts +9 -0
  51. package/dist/modules/dialog/DialogContainer.d.ts +37 -0
  52. package/dist/modules/dialog/Modal.d.ts +26 -0
  53. package/dist/modules/dialog/SwipeDialogContainer.d.ts +16 -0
  54. package/dist/modules/dialog/dialogAnimationUtils.d.ts +113 -0
  55. package/dist/modules/dialog/types.d.ts +183 -0
  56. package/dist/modules/dialog/useDialog.d.ts +39 -0
  57. package/dist/modules/dialog/useDialogContainer.d.ts +47 -0
  58. package/dist/modules/dialog/useDialogSwipeInput.d.ts +70 -0
  59. package/dist/modules/dialog/useDialogTransform.d.ts +82 -0
  60. package/dist/modules/drawer/types.d.ts +74 -0
  61. package/dist/modules/drawer/useDrawerSwipeInput.d.ts +24 -0
  62. package/dist/modules/pivot/SwipePivotTabBar.d.ts +3 -0
  63. package/dist/modules/stack/SwipeStackContent.d.ts +6 -3
  64. package/dist/modules/stack/SwipeStackOutlet.d.ts +4 -4
  65. package/dist/modules/stack/computeSwipeStackTransform.d.ts +1 -1
  66. package/dist/panels.cjs +1 -1
  67. package/dist/panels.js +1 -1
  68. package/dist/pivot.cjs +1 -1
  69. package/dist/pivot.js +1 -1
  70. package/dist/resizer.cjs +1 -1
  71. package/dist/resizer.js +2 -2
  72. package/dist/stack.cjs +1 -1
  73. package/dist/stack.cjs.map +1 -1
  74. package/dist/stack.js +503 -762
  75. package/dist/stack.js.map +1 -1
  76. package/dist/sticky-header/calculateStickyMetrics.d.ts +28 -0
  77. package/dist/sticky-header.cjs +1 -1
  78. package/dist/sticky-header.cjs.map +1 -1
  79. package/dist/sticky-header.js +59 -51
  80. package/dist/sticky-header.js.map +1 -1
  81. package/dist/{styles-DPPuJ0sf.js → styles-NkjuMOVS.js} +13 -13
  82. package/dist/{styles-DPPuJ0sf.js.map → styles-NkjuMOVS.js.map} +1 -1
  83. package/dist/styles-qf6ptVLD.cjs.map +1 -1
  84. package/dist/types.d.ts +16 -0
  85. package/dist/useDocumentPointerEvents-DXxw3qWj.js +54 -0
  86. package/dist/useDocumentPointerEvents-DXxw3qWj.js.map +1 -0
  87. package/dist/useDocumentPointerEvents-DxDSOtip.cjs +2 -0
  88. package/dist/useDocumentPointerEvents-DxDSOtip.cjs.map +1 -0
  89. package/dist/useNativeGestureGuard-C7TSqEkr.cjs +2 -0
  90. package/dist/useNativeGestureGuard-C7TSqEkr.cjs.map +1 -0
  91. package/dist/useNativeGestureGuard-CGYo6O0r.js +347 -0
  92. package/dist/useNativeGestureGuard-CGYo6O0r.js.map +1 -0
  93. package/dist/window/index.d.ts +2 -0
  94. package/dist/window.cjs +1 -1
  95. package/dist/window.cjs.map +1 -1
  96. package/dist/window.js +114 -103
  97. package/dist/window.js.map +1 -1
  98. package/package.json +6 -1
  99. package/src/components/gesture/SwipeSafeZone.tsx +69 -0
  100. package/src/components/window/Drawer.tsx +249 -162
  101. package/src/components/window/DrawerLayers.tsx +13 -3
  102. package/src/components/window/drawerStyles.spec.ts +263 -0
  103. package/src/components/window/drawerStyles.ts +228 -0
  104. package/src/components/window/drawerSwipeConfig.spec.ts +131 -0
  105. package/src/components/window/drawerSwipeConfig.ts +112 -0
  106. package/src/components/window/useDrawerSwipeTransform.spec.ts +234 -0
  107. package/src/components/window/useDrawerSwipeTransform.ts +129 -0
  108. package/src/constants/styles.ts +19 -0
  109. package/src/demo/pages/Dialog/alerts/index.tsx +22 -0
  110. package/src/demo/pages/Dialog/card/index.tsx +22 -0
  111. package/src/demo/pages/Dialog/components/AlertDialogDemo.tsx +124 -0
  112. package/src/demo/pages/Dialog/components/CardExpandDemo.module.css +243 -0
  113. package/src/demo/pages/Dialog/components/CardExpandDemo.tsx +204 -0
  114. package/src/demo/pages/Dialog/components/CustomAlertDialogDemo.tsx +219 -0
  115. package/src/demo/pages/Dialog/components/DialogDemos.module.css +77 -0
  116. package/src/demo/pages/Dialog/components/ModalBasics.tsx +45 -0
  117. package/src/demo/pages/Dialog/components/SwipeDialogDemo.module.css +77 -0
  118. package/src/demo/pages/Dialog/components/SwipeDialogDemo.tsx +181 -0
  119. package/src/demo/pages/Dialog/custom-alert/index.tsx +22 -0
  120. package/src/demo/pages/Dialog/modal/index.tsx +17 -0
  121. package/src/demo/pages/Dialog/swipe/index.tsx +22 -0
  122. package/src/demo/pages/Drawer/components/DrawerSwipe.module.css +316 -0
  123. package/src/demo/pages/Drawer/components/DrawerSwipe.tsx +178 -0
  124. package/src/demo/pages/Drawer/swipe/index.tsx +17 -0
  125. package/src/demo/pages/Pivot/components/SwipeTabsPivot.tsx +54 -23
  126. package/src/demo/pages/Pivot/swipe-debug/index.tsx +1 -1
  127. package/src/demo/pages/Stack/components/StackBasics.spec.tsx +152 -0
  128. package/src/demo/pages/Stack/components/StackBasics.tsx +179 -95
  129. package/src/demo/pages/Stack/components/StackTablet.spec.tsx +120 -0
  130. package/src/demo/pages/Stack/components/StackTablet.tsx +42 -21
  131. package/src/demo/routes.tsx +22 -1
  132. package/src/dialog/index.ts +85 -0
  133. package/src/hooks/gesture/testing/createGestureSimulator.spec.ts +68 -64
  134. package/src/hooks/gesture/testing/createGestureSimulator.ts +112 -37
  135. package/src/hooks/gesture/types.ts +83 -6
  136. package/src/hooks/gesture/useEdgeSwipeInput.spec.ts +22 -14
  137. package/src/hooks/gesture/useNativeGestureGuard.spec.ts +91 -31
  138. package/src/hooks/gesture/useNativeGestureGuard.ts +3 -1
  139. package/src/hooks/gesture/utils.ts +91 -0
  140. package/src/hooks/useAnimatedVisibility.spec.ts +44 -24
  141. package/src/hooks/useAnimatedVisibility.ts +28 -2
  142. package/src/hooks/useAnimationFrame.ts +8 -0
  143. package/src/hooks/useOperationContinuity.spec.ts +387 -0
  144. package/src/hooks/useOperationContinuity.ts +135 -0
  145. package/src/hooks/useResizeObserver.spec.tsx +277 -0
  146. package/src/hooks/useResizeObserver.tsx +108 -39
  147. package/src/hooks/useScrollContainer.ts +4 -10
  148. package/src/hooks/useSharedElementTransition.ts +333 -0
  149. package/src/hooks/useSwipeContentTransform.spec.ts +18 -18
  150. package/src/hooks/useSwipeContentTransform.ts +166 -28
  151. package/src/modules/dialog/AlertDialog.spec.tsx +387 -0
  152. package/src/modules/dialog/AlertDialog.tsx +221 -0
  153. package/src/modules/dialog/DialogContainer.spec.tsx +228 -0
  154. package/src/modules/dialog/DialogContainer.tsx +188 -0
  155. package/src/modules/dialog/Modal.spec.tsx +220 -0
  156. package/src/modules/dialog/Modal.tsx +182 -0
  157. package/src/modules/dialog/SwipeDialogContainer.tsx +208 -0
  158. package/src/modules/dialog/dialogAnimationUtils.spec.ts +253 -0
  159. package/src/modules/dialog/dialogAnimationUtils.ts +297 -0
  160. package/src/modules/dialog/types.ts +186 -0
  161. package/src/modules/dialog/useDialog.spec.tsx +447 -0
  162. package/src/modules/dialog/useDialog.ts +214 -0
  163. package/src/modules/dialog/useDialogContainer.spec.ts +331 -0
  164. package/src/modules/dialog/useDialogContainer.ts +150 -0
  165. package/src/modules/dialog/useDialogSwipeInput.spec.ts +157 -0
  166. package/src/modules/dialog/useDialogSwipeInput.ts +319 -0
  167. package/src/modules/dialog/useDialogTransform.spec.ts +370 -0
  168. package/src/modules/dialog/useDialogTransform.ts +407 -0
  169. package/src/modules/drawer/types.ts +102 -0
  170. package/src/modules/drawer/useDrawerSwipeInput.spec.ts +566 -0
  171. package/src/modules/drawer/useDrawerSwipeInput.ts +399 -0
  172. package/src/modules/panels/rendering/ContentRegistry.spec.tsx +21 -14
  173. package/src/modules/pivot/SwipePivotContent.position.spec.tsx +12 -8
  174. package/src/modules/pivot/SwipePivotContent.spec.tsx +55 -25
  175. package/src/modules/pivot/SwipePivotContent.tsx +2 -2
  176. package/src/modules/pivot/SwipePivotTabBar.spec.tsx +85 -68
  177. package/src/modules/pivot/SwipePivotTabBar.tsx +75 -15
  178. package/src/modules/pivot/scaleInputState.spec.ts +11 -2
  179. package/src/modules/pivot/usePivot.spec.ts +17 -3
  180. package/src/modules/pivot/usePivotSwipeInput.spec.ts +182 -123
  181. package/src/modules/stack/SwipeStackContent.spec.tsx +387 -100
  182. package/src/modules/stack/SwipeStackContent.tsx +43 -33
  183. package/src/modules/stack/SwipeStackOutlet.spec.tsx +14 -16
  184. package/src/modules/stack/SwipeStackOutlet.tsx +6 -6
  185. package/src/modules/stack/computeSwipeStackTransform.spec.ts +5 -5
  186. package/src/modules/stack/computeSwipeStackTransform.ts +3 -3
  187. package/src/modules/stack/swipeTransitionContinuity.spec.tsx +1133 -0
  188. package/src/modules/stack/useStackAnimationState.spec.ts +3 -1
  189. package/src/modules/stack/useStackAnimationState.ts +18 -13
  190. package/src/modules/stack/useStackNavigation.spec.ts +198 -3
  191. package/src/modules/stack/useStackNavigation.tsx +113 -56
  192. package/src/modules/stack/useStackSwipeInput.spec.ts +65 -32
  193. package/src/modules/stack/useStackSwipeInput.ts +1 -1
  194. package/src/sticky-header/StickyArea.tsx +29 -57
  195. package/src/sticky-header/calculateStickyMetrics.spec.ts +105 -0
  196. package/src/sticky-header/calculateStickyMetrics.ts +50 -0
  197. package/src/types.ts +18 -0
  198. package/src/window/index.ts +2 -0
  199. package/dist/FloatingWindow-BpdOpg_L.js +0 -400
  200. package/dist/FloatingWindow-BpdOpg_L.js.map +0 -1
  201. package/dist/FloatingWindow-TCDNY5gE.cjs +0 -2
  202. package/dist/FloatingWindow-TCDNY5gE.cjs.map +0 -1
  203. package/dist/GridLayout-B4VRsC0r.cjs +0 -2
  204. package/dist/ResizeHandle-CScipO5l.cjs +0 -2
  205. package/dist/SwipePivotTabBar-BGO9X94m.js +0 -407
  206. package/dist/SwipePivotTabBar-BGO9X94m.js.map +0 -1
  207. package/dist/SwipePivotTabBar-BrQismcZ.cjs +0 -2
  208. package/dist/SwipePivotTabBar-BrQismcZ.cjs.map +0 -1
  209. package/dist/useDocumentPointerEvents-CKdhGXd0.js +0 -46
  210. package/dist/useDocumentPointerEvents-CKdhGXd0.js.map +0 -1
  211. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs +0 -2
  212. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs.map +0 -1
  213. package/dist/useEffectEvent-Dp7HLCf0.js +0 -13
  214. package/dist/useEffectEvent-Dp7HLCf0.js.map +0 -1
  215. package/dist/useEffectEvent-huSsGUnl.cjs +0 -2
  216. package/dist/useEffectEvent-huSsGUnl.cjs.map +0 -1
@@ -7,7 +7,6 @@
7
7
  * - Same tab may appear at multiple slots (clones for infinite loop)
8
8
  * - Query by data-slot attribute for unique identification
9
9
  */
10
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
11
10
  import { render, screen, act } from "@testing-library/react";
12
11
  import * as React from "react";
13
12
  import { SwipePivotTabBar } from "./SwipePivotTabBar";
@@ -15,34 +14,59 @@ import type { IndicatorRenderProps } from "./SwipePivotTabBar";
15
14
  import type { SwipeInputState } from "../../hooks/gesture/types";
16
15
 
17
16
  // Mock requestAnimationFrame for animation testing
18
- let rafCallbacks: FrameRequestCallback[] = [];
19
- let rafId = 0;
17
+ const rafState = {
18
+ callbacks: [] as FrameRequestCallback[],
19
+ id: 0,
20
+ originalRAF: globalThis.requestAnimationFrame,
21
+ originalCAF: globalThis.cancelAnimationFrame,
22
+ };
20
23
 
21
- const mockRAF = vi.fn((callback: FrameRequestCallback) => {
22
- rafCallbacks.push(callback);
23
- return ++rafId;
24
- });
24
+ const resetRafState = (): void => {
25
+ rafState.callbacks = [];
26
+ rafState.id = 0;
27
+ };
25
28
 
26
- const mockCAF = vi.fn((id: number) => {
27
- // Remove callback if needed
28
- });
29
+ const mockRAF = (callback: FrameRequestCallback): number => {
30
+ rafState.callbacks = [...rafState.callbacks, callback];
31
+ rafState.id += 1;
32
+ return rafState.id;
33
+ };
34
+
35
+ const mockCAF = (id: number): void => {
36
+ void id;
37
+ };
38
+
39
+ const flushRAF = (): void => {
40
+ const callbacks = rafState.callbacks;
41
+ rafState.callbacks = [];
42
+ callbacks.forEach((cb) => cb(performance.now()));
43
+ };
29
44
 
30
- const flushRAF = () => {
31
- const callbacks = rafCallbacks;
32
- rafCallbacks = [];
33
- callbacks.forEach(cb => cb(performance.now()));
45
+ type RenderTracker<TArgs extends ReadonlyArray<unknown>, TResult> = {
46
+ calls: ReadonlyArray<TArgs>;
47
+ fn: (...args: TArgs) => TResult;
48
+ };
49
+
50
+ const createRenderTracker = <TArgs extends ReadonlyArray<unknown>, TResult>(
51
+ implementation: (...args: TArgs) => TResult,
52
+ ): RenderTracker<TArgs, TResult> => {
53
+ const calls: Array<TArgs> = [];
54
+ const fn = (...args: TArgs): TResult => {
55
+ calls.push(args);
56
+ return implementation(...args);
57
+ };
58
+ return { calls, fn };
34
59
  };
35
60
 
36
61
  beforeEach(() => {
37
- rafCallbacks = [];
38
- rafId = 0;
39
- vi.stubGlobal("requestAnimationFrame", mockRAF);
40
- vi.stubGlobal("cancelAnimationFrame", mockCAF);
62
+ resetRafState();
63
+ globalThis.requestAnimationFrame = mockRAF;
64
+ globalThis.cancelAnimationFrame = mockCAF;
41
65
  });
42
66
 
43
67
  afterEach(() => {
44
- vi.unstubAllGlobals();
45
- vi.clearAllMocks();
68
+ globalThis.requestAnimationFrame = rafState.originalRAF;
69
+ globalThis.cancelAnimationFrame = rafState.originalCAF;
46
70
  });
47
71
 
48
72
  const createItems = () => [
@@ -67,13 +91,6 @@ const swipingLeftState = (displacement: number): SwipeInputState => ({
67
91
  direction: -1,
68
92
  });
69
93
 
70
- const swipingRightState = (displacement: number): SwipeInputState => ({
71
- phase: "swiping",
72
- displacement: { x: displacement, y: 0 },
73
- velocity: { x: 0.5, y: 0 },
74
- direction: 1,
75
- });
76
-
77
94
  const endedState = (direction: -1 | 0 | 1): SwipeInputState => ({
78
95
  phase: "ended",
79
96
  displacement: { x: 0, y: 0 },
@@ -102,10 +119,6 @@ const getSlot = (container: HTMLElement, slotPosition: number): HTMLElement | nu
102
119
  };
103
120
 
104
121
  // Helper to get all visible slots
105
- const getVisibleSlots = (container: HTMLElement): HTMLElement[] => {
106
- return Array.from(container.querySelectorAll('[data-slot][style*="visibility: visible"]'));
107
- };
108
-
109
122
  describe("SwipePivotTabBar", () => {
110
123
  describe("Initial render", () => {
111
124
  it("renders tabs at slot positions", () => {
@@ -609,7 +622,9 @@ describe("SwipePivotTabBar", () => {
609
622
 
610
623
  describe("Sliding indicator (iOS-style)", () => {
611
624
  it("renders indicator with correct offset props", () => {
612
- const indicatorFn = vi.fn(() => <div data-testid="indicator" />);
625
+ const indicatorTracker = createRenderTracker<readonly [IndicatorRenderProps], React.ReactNode>(() => (
626
+ <div data-testid="indicator" />
627
+ ));
613
628
 
614
629
  render(
615
630
  <SwipePivotTabBar
@@ -618,11 +633,12 @@ describe("SwipePivotTabBar", () => {
618
633
  activeIndex={0}
619
634
  itemCount={5}
620
635
  inputState={idleState}
621
- renderIndicator={indicatorFn}
636
+ renderIndicator={indicatorTracker.fn}
622
637
  />
623
638
  );
624
639
 
625
- expect(indicatorFn).toHaveBeenCalledWith({
640
+ expect(indicatorTracker.calls).toHaveLength(1);
641
+ expect(indicatorTracker.calls[0]?.[0]).toEqual({
626
642
  offsetPx: 0,
627
643
  tabWidth: 100,
628
644
  centerX: 200,
@@ -634,7 +650,9 @@ describe("SwipePivotTabBar", () => {
634
650
  });
635
651
 
636
652
  it("passes swipe displacement to indicator", () => {
637
- const indicatorFn = vi.fn((_props: IndicatorRenderProps) => <div data-testid="indicator" />);
653
+ const indicatorTracker = createRenderTracker<readonly [IndicatorRenderProps], React.ReactNode>(() => (
654
+ <div data-testid="indicator" />
655
+ ));
638
656
 
639
657
  const { rerender } = render(
640
658
  <SwipePivotTabBar
@@ -643,7 +661,7 @@ describe("SwipePivotTabBar", () => {
643
661
  activeIndex={0}
644
662
  itemCount={5}
645
663
  inputState={idleState}
646
- renderIndicator={indicatorFn}
664
+ renderIndicator={indicatorTracker.fn}
647
665
  />
648
666
  );
649
667
 
@@ -654,20 +672,19 @@ describe("SwipePivotTabBar", () => {
654
672
  activeIndex={0}
655
673
  itemCount={5}
656
674
  inputState={swipingLeftState(-60)}
657
- renderIndicator={indicatorFn}
675
+ renderIndicator={indicatorTracker.fn}
658
676
  />
659
677
  );
660
678
 
661
679
  // Last call should have the swipe offset
662
- const calls = indicatorFn.mock.calls;
663
- expect(calls.length).toBeGreaterThan(0);
664
- const lastCall = calls[calls.length - 1]![0];
680
+ expect(indicatorTracker.calls.length).toBeGreaterThan(0);
681
+ const lastCall = indicatorTracker.calls[indicatorTracker.calls.length - 1]![0];
665
682
  expect(lastCall.offsetPx).toBe(-60);
666
683
  expect(lastCall.isSwiping).toBe(true);
667
684
  });
668
685
 
669
686
  it("indicator follows same offset as tabs", () => {
670
- let indicatorOffset = 0;
687
+ const indicatorState = { offset: 0 };
671
688
 
672
689
  const { container, rerender } = render(
673
690
  <SwipePivotTabBar
@@ -677,7 +694,7 @@ describe("SwipePivotTabBar", () => {
677
694
  itemCount={5}
678
695
  inputState={idleState}
679
696
  renderIndicator={({ offsetPx }) => {
680
- indicatorOffset = offsetPx;
697
+ indicatorState.offset = offsetPx;
681
698
  return <div data-testid="indicator" />;
682
699
  }}
683
700
  />
@@ -691,7 +708,7 @@ describe("SwipePivotTabBar", () => {
691
708
  itemCount={5}
692
709
  inputState={swipingLeftState(-80)}
693
710
  renderIndicator={({ offsetPx }) => {
694
- indicatorOffset = offsetPx;
711
+ indicatorState.offset = offsetPx;
695
712
  return <div data-testid="indicator" />;
696
713
  }}
697
714
  />
@@ -700,7 +717,7 @@ describe("SwipePivotTabBar", () => {
700
717
  // Verify indicator offset matches tab offset
701
718
  const slot0 = getSlot(container, 0);
702
719
  expect(slot0).toHaveStyle({ transform: "translateX(-80px)" });
703
- expect(indicatorOffset).toBe(-80);
720
+ expect(indicatorState.offset).toBe(-80);
704
721
  });
705
722
  });
706
723
 
@@ -742,8 +759,7 @@ describe("SwipePivotTabBar", () => {
742
759
 
743
760
  describe("Fixed tabs mode (iOS segmented control style)", () => {
744
761
  it("tabs stay fixed during swipe, only indicator moves", () => {
745
- let indicatorOffset = 0;
746
- let indicatorCenterX = 0;
762
+ const indicatorState = { offset: 0, centerX: 0 };
747
763
 
748
764
  const { container, rerender } = render(
749
765
  <SwipePivotTabBar
@@ -754,8 +770,8 @@ describe("SwipePivotTabBar", () => {
754
770
  fixedTabs={true}
755
771
  inputState={idleState}
756
772
  renderIndicator={({ offsetPx, centerX }) => {
757
- indicatorOffset = offsetPx;
758
- indicatorCenterX = centerX;
773
+ indicatorState.offset = offsetPx;
774
+ indicatorState.centerX = centerX;
759
775
  return <div data-testid="indicator" />;
760
776
  }}
761
777
  />
@@ -769,8 +785,8 @@ describe("SwipePivotTabBar", () => {
769
785
  // viewportWidth=500, 5 tabs * 100px = 500px, centeringOffset = (500-500)/2 = 0
770
786
  // centerX is fixed at centeringOffset = 0
771
787
  // offsetPx = activeIndex * tabWidth = 0 * 100 = 0
772
- expect(indicatorCenterX).toBe(0);
773
- expect(indicatorOffset).toBe(0);
788
+ expect(indicatorState.centerX).toBe(0);
789
+ expect(indicatorState.offset).toBe(0);
774
790
 
775
791
  // During swipe, tabs should NOT have transform (they're fixed)
776
792
  rerender(
@@ -782,8 +798,8 @@ describe("SwipePivotTabBar", () => {
782
798
  fixedTabs={true}
783
799
  inputState={swipingLeftState(-80)}
784
800
  renderIndicator={({ offsetPx, centerX }) => {
785
- indicatorOffset = offsetPx;
786
- indicatorCenterX = centerX;
801
+ indicatorState.offset = offsetPx;
802
+ indicatorState.centerX = centerX;
787
803
  return <div data-testid="indicator" />;
788
804
  }}
789
805
  />
@@ -791,7 +807,7 @@ describe("SwipePivotTabBar", () => {
791
807
 
792
808
  // Indicator should move OPPOSITE to swipe direction
793
809
  // Swipe left (displacement = -80) → indicator moves right (+80)
794
- expect(indicatorOffset).toBe(80);
810
+ expect(indicatorState.offset).toBe(80);
795
811
 
796
812
  // Tabs should not have any transform applied (position is relative, not absolute with transform)
797
813
  const tabsAfterSwipe = container.querySelectorAll("[data-pivot-tab]");
@@ -804,12 +820,12 @@ describe("SwipePivotTabBar", () => {
804
820
  });
805
821
 
806
822
  it("indicator moves to new tab position when activeIndex changes", () => {
807
- let indicatorOffsetPx = 0;
808
- let indicatorCenterX = 0;
823
+ const indicatorState = { offsetPx: 0, centerX: 0 };
809
824
 
810
825
  // Mock performance.now to control animation timing
811
- let mockTime = 0;
812
- vi.spyOn(performance, "now").mockImplementation(() => mockTime);
826
+ const originalNow = performance.now;
827
+ const timeState = { value: 0 };
828
+ performance.now = () => timeState.value;
813
829
 
814
830
  const { rerender } = render(
815
831
  <SwipePivotTabBar
@@ -820,17 +836,17 @@ describe("SwipePivotTabBar", () => {
820
836
  fixedTabs={true}
821
837
  inputState={idleState}
822
838
  renderIndicator={({ offsetPx, centerX }) => {
823
- indicatorOffsetPx = offsetPx;
824
- indicatorCenterX = centerX;
839
+ indicatorState.offsetPx = offsetPx;
840
+ indicatorState.centerX = centerX;
825
841
  return <div data-testid="indicator" />;
826
842
  }}
827
843
  />
828
844
  );
829
845
 
830
846
  // centerX is fixed at centering offset (0 for 5 tabs * 100px = 500px viewport)
831
- expect(indicatorCenterX).toBe(0);
847
+ expect(indicatorState.centerX).toBe(0);
832
848
  // offsetPx includes active tab position
833
- expect(indicatorOffsetPx).toBe(0); // Tab 1 at position 0
849
+ expect(indicatorState.offsetPx).toBe(0); // Tab 1 at position 0
834
850
 
835
851
  rerender(
836
852
  <SwipePivotTabBar
@@ -841,25 +857,26 @@ describe("SwipePivotTabBar", () => {
841
857
  fixedTabs={true}
842
858
  inputState={idleState}
843
859
  renderIndicator={({ offsetPx, centerX }) => {
844
- indicatorOffsetPx = offsetPx;
845
- indicatorCenterX = centerX;
860
+ indicatorState.offsetPx = offsetPx;
861
+ indicatorState.centerX = centerX;
846
862
  return <div data-testid="indicator" />;
847
863
  }}
848
864
  />
849
865
  );
850
866
 
851
867
  // Animation starts - flush RAF callbacks until animation completes
852
- mockTime = 500; // Advance past animation duration (300ms default)
868
+ timeState.value = 500; // Advance past animation duration (300ms default)
853
869
  act(() => {
854
- for (let i = 0; i < 10; i++) {
870
+ Array.from({ length: 10 }).forEach(() => {
855
871
  flushRAF();
856
- }
872
+ });
857
873
  });
858
874
 
859
875
  // centerX stays fixed
860
- expect(indicatorCenterX).toBe(0);
876
+ expect(indicatorState.centerX).toBe(0);
861
877
  // offsetPx now includes Tab 3 position (after animation completes)
862
- expect(indicatorOffsetPx).toBe(200); // Tab 3 at position 2 * 100
878
+ expect(indicatorState.offsetPx).toBe(200); // Tab 3 at position 2 * 100
879
+ performance.now = originalNow;
863
880
  });
864
881
  });
865
882
  });
@@ -113,8 +113,18 @@ const getItemAtPosition = (
113
113
  ): number | null => {
114
114
  const targetIndex = activeIndex + slotPosition;
115
115
 
116
+ const isOutOfRange = (index: number, count: number): boolean => {
117
+ if (index < 0) {
118
+ return true;
119
+ }
120
+ if (index >= count) {
121
+ return true;
122
+ }
123
+ return false;
124
+ };
125
+
116
126
  if (navigationMode === "linear") {
117
- if (targetIndex < 0 || targetIndex >= itemCount) {
127
+ if (isOutOfRange(targetIndex, itemCount)) {
118
128
  return null;
119
129
  }
120
130
  return targetIndex;
@@ -190,6 +200,9 @@ const easeOutExpo = (t: number): number => {
190
200
  return t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
191
201
  };
192
202
 
203
+ /**
204
+ * Swipeable tab bar for pivot navigation.
205
+ */
193
206
  export function SwipePivotTabBar<TId extends string = string>({
194
207
  items,
195
208
  activeId,
@@ -205,8 +218,49 @@ export function SwipePivotTabBar<TId extends string = string>({
205
218
  fixedTabs = false,
206
219
  renderIndicator,
207
220
  }: SwipePivotTabBarProps<TId>): React.ReactElement {
221
+ const isSwipePhase = (phase: SwipeInputState["phase"]): boolean => {
222
+ if (phase === "swiping") {
223
+ return true;
224
+ }
225
+ if (phase === "tracking") {
226
+ return true;
227
+ }
228
+ return false;
229
+ };
230
+
231
+ const getIsAnimating = (
232
+ slotAnimation: typeof animationRef.current,
233
+ fixedAnimation: typeof fixedAnimationRef.current,
234
+ ): boolean => {
235
+ if (slotAnimation !== null) {
236
+ return true;
237
+ }
238
+ if (fixedAnimation !== null) {
239
+ return true;
240
+ }
241
+ return false;
242
+ };
243
+
244
+ const getDelta = (
245
+ mode: "linear" | "loop",
246
+ nextIndex: number,
247
+ previousIndex: number,
248
+ totalItems: number,
249
+ ): number => {
250
+ if (mode === "loop") {
251
+ // Use shortest path in loop mode
252
+ const forwardDist = normalizeIndex(nextIndex - previousIndex, totalItems);
253
+ const backwardDist = totalItems - forwardDist;
254
+ if (forwardDist <= backwardDist) {
255
+ return forwardDist;
256
+ }
257
+ return -backwardDist;
258
+ }
259
+ return nextIndex - previousIndex;
260
+ };
261
+
208
262
  const displacement = getAxisDisplacement(inputState, axis);
209
- const isSwiping = inputState.phase === "swiping" || inputState.phase === "tracking";
263
+ const isSwiping = isSwipePhase(inputState.phase);
210
264
 
211
265
  // ============================================================
212
266
  // Animation state for SLOT-BASED mode (scrolling tabs)
@@ -247,7 +301,12 @@ export function SwipePivotTabBar<TId extends string = string>({
247
301
  // Fixed tabs mode: track swipe position
248
302
  // ============================================================
249
303
  React.useEffect(() => {
250
- if (!fixedTabs || !isSwiping) return;
304
+ if (!fixedTabs) {
305
+ return;
306
+ }
307
+ if (!isSwiping) {
308
+ return;
309
+ }
251
310
 
252
311
  // During swipe, track the visual position
253
312
  // Swipe direction is OPPOSITE to indicator movement
@@ -260,7 +319,12 @@ export function SwipePivotTabBar<TId extends string = string>({
260
319
  // Fixed tabs mode: animate when swipe ends or tab clicked
261
320
  // ============================================================
262
321
  React.useEffect(() => {
263
- if (!fixedTabs || isSwiping) return;
322
+ if (!fixedTabs) {
323
+ return;
324
+ }
325
+ if (isSwiping) {
326
+ return;
327
+ }
264
328
 
265
329
  // When swipe ends or tab changes via click
266
330
  const targetPosition = activeIndex * tabWidth;
@@ -309,7 +373,9 @@ export function SwipePivotTabBar<TId extends string = string>({
309
373
  // Slot-based mode animation: handle activeIndex changes
310
374
  // ============================================================
311
375
  React.useEffect(() => {
312
- if (fixedTabs) return; // Skip for fixed tabs mode
376
+ if (fixedTabs) {
377
+ return; // Skip for fixed tabs mode
378
+ }
313
379
 
314
380
  if (prevActiveIndexRef.current === activeIndex) {
315
381
  return;
@@ -319,15 +385,7 @@ export function SwipePivotTabBar<TId extends string = string>({
319
385
  prevActiveIndexRef.current = activeIndex;
320
386
 
321
387
  // Calculate direction of movement
322
- let delta: number;
323
- if (navigationMode === "loop") {
324
- // Use shortest path in loop mode
325
- const forwardDist = normalizeIndex(activeIndex - prevIndex, itemCount);
326
- const backwardDist = itemCount - forwardDist;
327
- delta = forwardDist <= backwardDist ? forwardDist : -backwardDist;
328
- } else {
329
- delta = activeIndex - prevIndex;
330
- }
388
+ const delta = getDelta(navigationMode, activeIndex, prevIndex, itemCount);
331
389
 
332
390
  // Target offset to animate to (then snap to 0)
333
391
  const targetOffsetPx = -delta * tabWidth;
@@ -407,7 +465,7 @@ export function SwipePivotTabBar<TId extends string = string>({
407
465
 
408
466
  // Current offset for slot-based mode
409
467
  const currentOffset = isSwiping ? displacement : animatedOffset;
410
- const isAnimating = animationRef.current !== null || fixedAnimationRef.current !== null;
468
+ const isAnimating = getIsAnimating(animationRef.current, fixedAnimationRef.current);
411
469
 
412
470
  // Cancel slot animation when swiping starts
413
471
  React.useEffect(() => {
@@ -428,6 +486,7 @@ export function SwipePivotTabBar<TId extends string = string>({
428
486
 
429
487
  return (
430
488
  <div
489
+ data-active-id={activeId}
431
490
  style={{
432
491
  position: "relative",
433
492
  width: "100%",
@@ -486,6 +545,7 @@ export function SwipePivotTabBar<TId extends string = string>({
486
545
 
487
546
  return (
488
547
  <div
548
+ data-active-id={activeId}
489
549
  style={{
490
550
  position: "relative",
491
551
  width: "100%",
@@ -1,16 +1,25 @@
1
1
  /**
2
2
  * @file Tests for scaleInputState utility
3
3
  */
4
- import { describe, it, expect } from "vitest";
5
4
  import { scaleInputState } from "./scaleInputState";
6
5
  import type { SwipeInputState } from "../../hooks/gesture/types";
7
6
 
8
7
  describe("scaleInputState", () => {
8
+ const getDirection = (value: number): -1 | 0 | 1 => {
9
+ if (value > 0) {
10
+ return 1;
11
+ }
12
+ if (value < 0) {
13
+ return -1;
14
+ }
15
+ return 0;
16
+ };
17
+
9
18
  const createSwipingState = (x: number, vx: number): SwipeInputState => ({
10
19
  phase: "swiping",
11
20
  displacement: { x, y: 0 },
12
21
  velocity: { x: vx, y: 0 },
13
- direction: x > 0 ? 1 : x < 0 ? -1 : 0,
22
+ direction: getDirection(x),
14
23
  });
15
24
 
16
25
  describe("scaling factor calculation", () => {
@@ -6,6 +6,19 @@ import { usePivot } from "./usePivot.js";
6
6
  import type { PivotItem } from "./types.js";
7
7
 
8
8
  describe("usePivot", () => {
9
+ type CallTracker = {
10
+ calls: ReadonlyArray<ReadonlyArray<unknown>>;
11
+ fn: (...args: ReadonlyArray<unknown>) => void;
12
+ };
13
+
14
+ const createCallTracker = (): CallTracker => {
15
+ const calls: Array<ReadonlyArray<unknown>> = [];
16
+ const fn = (...args: ReadonlyArray<unknown>): void => {
17
+ calls.push(args);
18
+ };
19
+ return { calls, fn };
20
+ };
21
+
9
22
  const createItems = (): ReadonlyArray<PivotItem<"a" | "b" | "c">> => [
10
23
  { id: "a", label: "Item A", content: "Content A" },
11
24
  { id: "b", label: "Item B", content: "Content B" },
@@ -244,16 +257,17 @@ describe("usePivot", () => {
244
257
  describe("onActiveChange callback", () => {
245
258
  it("calls onActiveChange when navigating with go", () => {
246
259
  const items = createItems();
247
- const onActiveChange = vi.fn();
260
+ const onActiveChange = createCallTracker();
248
261
  const { result } = renderHook(() =>
249
- usePivot({ items, defaultActiveId: "a", onActiveChange }),
262
+ usePivot({ items, defaultActiveId: "a", onActiveChange: onActiveChange.fn }),
250
263
  );
251
264
 
252
265
  act(() => {
253
266
  result.current.go(1);
254
267
  });
255
268
 
256
- expect(onActiveChange).toHaveBeenCalledWith("b");
269
+ expect(onActiveChange.calls).toHaveLength(1);
270
+ expect(onActiveChange.calls[0]?.[0]).toBe("b");
257
271
  });
258
272
  });
259
273