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.
- package/dist/{FloatingPanelFrame-SgYLc6Ud.js → FloatingPanelFrame-3eU9AwPo.js} +2 -2
- package/dist/{FloatingPanelFrame-SgYLc6Ud.js.map → FloatingPanelFrame-3eU9AwPo.js.map} +1 -1
- package/dist/FloatingWindow-CUXnEtrb.js +827 -0
- package/dist/FloatingWindow-CUXnEtrb.js.map +1 -0
- package/dist/FloatingWindow-DMwyK0eK.cjs +2 -0
- package/dist/FloatingWindow-DMwyK0eK.cjs.map +1 -0
- package/dist/GridLayout-DKTg_N61.cjs +2 -0
- package/dist/{GridLayout-B4VRsC0r.cjs.map → GridLayout-DKTg_N61.cjs.map} +1 -1
- package/dist/{GridLayout-BltqeCPK.js → GridLayout-UWNxXw77.js} +34 -35
- package/dist/{GridLayout-BltqeCPK.js.map → GridLayout-UWNxXw77.js.map} +1 -1
- package/dist/{HorizontalDivider-WF1k_qND.js → HorizontalDivider-DdxzfV0l.js} +3 -3
- package/dist/{HorizontalDivider-WF1k_qND.js.map → HorizontalDivider-DdxzfV0l.js.map} +1 -1
- package/dist/{HorizontalDivider-B5Z-KZLk.cjs → HorizontalDivider-_pgV4Mcv.cjs} +2 -2
- package/dist/{HorizontalDivider-B5Z-KZLk.cjs.map → HorizontalDivider-_pgV4Mcv.cjs.map} +1 -1
- package/dist/{PanelSystem-Dr1TBhxM.js → PanelSystem-BqUzNtf2.js} +5 -5
- package/dist/{PanelSystem-Dr1TBhxM.js.map → PanelSystem-BqUzNtf2.js.map} +1 -1
- package/dist/{PanelSystem-Bs8bQwQF.cjs → PanelSystem-D603LKKv.cjs} +2 -2
- package/dist/{PanelSystem-Bs8bQwQF.cjs.map → PanelSystem-D603LKKv.cjs.map} +1 -1
- package/dist/ResizeHandle-CBcAS918.cjs +2 -0
- package/dist/{ResizeHandle-CScipO5l.cjs.map → ResizeHandle-CBcAS918.cjs.map} +1 -1
- package/dist/{ResizeHandle-CdA_JYfN.js → ResizeHandle-CXjc1meV.js} +28 -29
- package/dist/{ResizeHandle-CdA_JYfN.js.map → ResizeHandle-CXjc1meV.js.map} +1 -1
- package/dist/SwipePivotTabBar-DWrCuwEI.js +411 -0
- package/dist/SwipePivotTabBar-DWrCuwEI.js.map +1 -0
- package/dist/SwipePivotTabBar-fjjXkpj7.cjs +2 -0
- package/dist/SwipePivotTabBar-fjjXkpj7.cjs.map +1 -0
- package/dist/components/gesture/SwipeSafeZone.d.ts +40 -0
- package/dist/components/window/Drawer.d.ts +3 -1
- package/dist/components/window/DrawerLayers.d.ts +1 -1
- package/dist/components/window/drawerStyles.d.ts +69 -0
- package/dist/components/window/drawerSwipeConfig.d.ts +29 -0
- package/dist/components/window/useDrawerSwipeTransform.d.ts +23 -0
- package/dist/config.cjs +1 -1
- package/dist/config.js +3 -3
- package/dist/constants/styles.d.ts +17 -0
- package/dist/dialog/index.d.ts +69 -0
- package/dist/floating.js +1 -1
- package/dist/grid.cjs +1 -1
- package/dist/grid.js +2 -2
- package/dist/hooks/gesture/testing/createGestureSimulator.d.ts +7 -0
- package/dist/hooks/gesture/types.d.ts +48 -5
- package/dist/hooks/gesture/utils.d.ts +19 -0
- package/dist/hooks/useAnimationFrame.d.ts +2 -0
- package/dist/hooks/useOperationContinuity.d.ts +64 -0
- package/dist/hooks/useResizeObserver.d.ts +33 -1
- package/dist/hooks/useSharedElementTransition.d.ts +112 -0
- package/dist/hooks/useSwipeContentTransform.d.ts +9 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +7 -7
- package/dist/modules/dialog/AlertDialog.d.ts +9 -0
- package/dist/modules/dialog/DialogContainer.d.ts +37 -0
- package/dist/modules/dialog/Modal.d.ts +26 -0
- package/dist/modules/dialog/SwipeDialogContainer.d.ts +16 -0
- package/dist/modules/dialog/dialogAnimationUtils.d.ts +113 -0
- package/dist/modules/dialog/types.d.ts +183 -0
- package/dist/modules/dialog/useDialog.d.ts +39 -0
- package/dist/modules/dialog/useDialogContainer.d.ts +47 -0
- package/dist/modules/dialog/useDialogSwipeInput.d.ts +70 -0
- package/dist/modules/dialog/useDialogTransform.d.ts +82 -0
- package/dist/modules/drawer/types.d.ts +74 -0
- package/dist/modules/drawer/useDrawerSwipeInput.d.ts +24 -0
- package/dist/modules/pivot/SwipePivotTabBar.d.ts +3 -0
- package/dist/modules/stack/SwipeStackContent.d.ts +6 -3
- package/dist/modules/stack/SwipeStackOutlet.d.ts +4 -4
- package/dist/modules/stack/computeSwipeStackTransform.d.ts +1 -1
- package/dist/panels.cjs +1 -1
- package/dist/panels.js +1 -1
- package/dist/pivot.cjs +1 -1
- package/dist/pivot.js +1 -1
- package/dist/resizer.cjs +1 -1
- package/dist/resizer.js +2 -2
- package/dist/stack.cjs +1 -1
- package/dist/stack.cjs.map +1 -1
- package/dist/stack.js +503 -762
- package/dist/stack.js.map +1 -1
- package/dist/sticky-header/calculateStickyMetrics.d.ts +28 -0
- package/dist/sticky-header.cjs +1 -1
- package/dist/sticky-header.cjs.map +1 -1
- package/dist/sticky-header.js +59 -51
- package/dist/sticky-header.js.map +1 -1
- package/dist/{styles-DPPuJ0sf.js → styles-NkjuMOVS.js} +13 -13
- package/dist/{styles-DPPuJ0sf.js.map → styles-NkjuMOVS.js.map} +1 -1
- package/dist/styles-qf6ptVLD.cjs.map +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/useDocumentPointerEvents-DXxw3qWj.js +54 -0
- package/dist/useDocumentPointerEvents-DXxw3qWj.js.map +1 -0
- package/dist/useDocumentPointerEvents-DxDSOtip.cjs +2 -0
- package/dist/useDocumentPointerEvents-DxDSOtip.cjs.map +1 -0
- package/dist/useNativeGestureGuard-C7TSqEkr.cjs +2 -0
- package/dist/useNativeGestureGuard-C7TSqEkr.cjs.map +1 -0
- package/dist/useNativeGestureGuard-CGYo6O0r.js +347 -0
- package/dist/useNativeGestureGuard-CGYo6O0r.js.map +1 -0
- package/dist/window/index.d.ts +2 -0
- package/dist/window.cjs +1 -1
- package/dist/window.cjs.map +1 -1
- package/dist/window.js +114 -103
- package/dist/window.js.map +1 -1
- package/package.json +6 -1
- package/src/components/gesture/SwipeSafeZone.tsx +69 -0
- package/src/components/window/Drawer.tsx +249 -162
- package/src/components/window/DrawerLayers.tsx +13 -3
- package/src/components/window/drawerStyles.spec.ts +263 -0
- package/src/components/window/drawerStyles.ts +228 -0
- package/src/components/window/drawerSwipeConfig.spec.ts +131 -0
- package/src/components/window/drawerSwipeConfig.ts +112 -0
- package/src/components/window/useDrawerSwipeTransform.spec.ts +234 -0
- package/src/components/window/useDrawerSwipeTransform.ts +129 -0
- package/src/constants/styles.ts +19 -0
- package/src/demo/pages/Dialog/alerts/index.tsx +22 -0
- package/src/demo/pages/Dialog/card/index.tsx +22 -0
- package/src/demo/pages/Dialog/components/AlertDialogDemo.tsx +124 -0
- package/src/demo/pages/Dialog/components/CardExpandDemo.module.css +243 -0
- package/src/demo/pages/Dialog/components/CardExpandDemo.tsx +204 -0
- package/src/demo/pages/Dialog/components/CustomAlertDialogDemo.tsx +219 -0
- package/src/demo/pages/Dialog/components/DialogDemos.module.css +77 -0
- package/src/demo/pages/Dialog/components/ModalBasics.tsx +45 -0
- package/src/demo/pages/Dialog/components/SwipeDialogDemo.module.css +77 -0
- package/src/demo/pages/Dialog/components/SwipeDialogDemo.tsx +181 -0
- package/src/demo/pages/Dialog/custom-alert/index.tsx +22 -0
- package/src/demo/pages/Dialog/modal/index.tsx +17 -0
- package/src/demo/pages/Dialog/swipe/index.tsx +22 -0
- package/src/demo/pages/Drawer/components/DrawerSwipe.module.css +316 -0
- package/src/demo/pages/Drawer/components/DrawerSwipe.tsx +178 -0
- package/src/demo/pages/Drawer/swipe/index.tsx +17 -0
- package/src/demo/pages/Pivot/components/SwipeTabsPivot.tsx +54 -23
- package/src/demo/pages/Pivot/swipe-debug/index.tsx +1 -1
- package/src/demo/pages/Stack/components/StackBasics.spec.tsx +152 -0
- package/src/demo/pages/Stack/components/StackBasics.tsx +179 -95
- package/src/demo/pages/Stack/components/StackTablet.spec.tsx +120 -0
- package/src/demo/pages/Stack/components/StackTablet.tsx +42 -21
- package/src/demo/routes.tsx +22 -1
- package/src/dialog/index.ts +85 -0
- package/src/hooks/gesture/testing/createGestureSimulator.spec.ts +68 -64
- package/src/hooks/gesture/testing/createGestureSimulator.ts +112 -37
- package/src/hooks/gesture/types.ts +83 -6
- package/src/hooks/gesture/useEdgeSwipeInput.spec.ts +22 -14
- package/src/hooks/gesture/useNativeGestureGuard.spec.ts +91 -31
- package/src/hooks/gesture/useNativeGestureGuard.ts +3 -1
- package/src/hooks/gesture/utils.ts +91 -0
- package/src/hooks/useAnimatedVisibility.spec.ts +44 -24
- package/src/hooks/useAnimatedVisibility.ts +28 -2
- package/src/hooks/useAnimationFrame.ts +8 -0
- package/src/hooks/useOperationContinuity.spec.ts +387 -0
- package/src/hooks/useOperationContinuity.ts +135 -0
- package/src/hooks/useResizeObserver.spec.tsx +277 -0
- package/src/hooks/useResizeObserver.tsx +108 -39
- package/src/hooks/useScrollContainer.ts +4 -10
- package/src/hooks/useSharedElementTransition.ts +333 -0
- package/src/hooks/useSwipeContentTransform.spec.ts +18 -18
- package/src/hooks/useSwipeContentTransform.ts +166 -28
- package/src/modules/dialog/AlertDialog.spec.tsx +387 -0
- package/src/modules/dialog/AlertDialog.tsx +221 -0
- package/src/modules/dialog/DialogContainer.spec.tsx +228 -0
- package/src/modules/dialog/DialogContainer.tsx +188 -0
- package/src/modules/dialog/Modal.spec.tsx +220 -0
- package/src/modules/dialog/Modal.tsx +182 -0
- package/src/modules/dialog/SwipeDialogContainer.tsx +208 -0
- package/src/modules/dialog/dialogAnimationUtils.spec.ts +253 -0
- package/src/modules/dialog/dialogAnimationUtils.ts +297 -0
- package/src/modules/dialog/types.ts +186 -0
- package/src/modules/dialog/useDialog.spec.tsx +447 -0
- package/src/modules/dialog/useDialog.ts +214 -0
- package/src/modules/dialog/useDialogContainer.spec.ts +331 -0
- package/src/modules/dialog/useDialogContainer.ts +150 -0
- package/src/modules/dialog/useDialogSwipeInput.spec.ts +157 -0
- package/src/modules/dialog/useDialogSwipeInput.ts +319 -0
- package/src/modules/dialog/useDialogTransform.spec.ts +370 -0
- package/src/modules/dialog/useDialogTransform.ts +407 -0
- package/src/modules/drawer/types.ts +102 -0
- package/src/modules/drawer/useDrawerSwipeInput.spec.ts +566 -0
- package/src/modules/drawer/useDrawerSwipeInput.ts +399 -0
- package/src/modules/panels/rendering/ContentRegistry.spec.tsx +21 -14
- package/src/modules/pivot/SwipePivotContent.position.spec.tsx +12 -8
- package/src/modules/pivot/SwipePivotContent.spec.tsx +55 -25
- package/src/modules/pivot/SwipePivotContent.tsx +2 -2
- package/src/modules/pivot/SwipePivotTabBar.spec.tsx +85 -68
- package/src/modules/pivot/SwipePivotTabBar.tsx +75 -15
- package/src/modules/pivot/scaleInputState.spec.ts +11 -2
- package/src/modules/pivot/usePivot.spec.ts +17 -3
- package/src/modules/pivot/usePivotSwipeInput.spec.ts +182 -123
- package/src/modules/stack/SwipeStackContent.spec.tsx +387 -100
- package/src/modules/stack/SwipeStackContent.tsx +43 -33
- package/src/modules/stack/SwipeStackOutlet.spec.tsx +14 -16
- package/src/modules/stack/SwipeStackOutlet.tsx +6 -6
- package/src/modules/stack/computeSwipeStackTransform.spec.ts +5 -5
- package/src/modules/stack/computeSwipeStackTransform.ts +3 -3
- package/src/modules/stack/swipeTransitionContinuity.spec.tsx +1133 -0
- package/src/modules/stack/useStackAnimationState.spec.ts +3 -1
- package/src/modules/stack/useStackAnimationState.ts +18 -13
- package/src/modules/stack/useStackNavigation.spec.ts +198 -3
- package/src/modules/stack/useStackNavigation.tsx +113 -56
- package/src/modules/stack/useStackSwipeInput.spec.ts +65 -32
- package/src/modules/stack/useStackSwipeInput.ts +1 -1
- package/src/sticky-header/StickyArea.tsx +29 -57
- package/src/sticky-header/calculateStickyMetrics.spec.ts +105 -0
- package/src/sticky-header/calculateStickyMetrics.ts +50 -0
- package/src/types.ts +18 -0
- package/src/window/index.ts +2 -0
- package/dist/FloatingWindow-BpdOpg_L.js +0 -400
- package/dist/FloatingWindow-BpdOpg_L.js.map +0 -1
- package/dist/FloatingWindow-TCDNY5gE.cjs +0 -2
- package/dist/FloatingWindow-TCDNY5gE.cjs.map +0 -1
- package/dist/GridLayout-B4VRsC0r.cjs +0 -2
- package/dist/ResizeHandle-CScipO5l.cjs +0 -2
- package/dist/SwipePivotTabBar-BGO9X94m.js +0 -407
- package/dist/SwipePivotTabBar-BGO9X94m.js.map +0 -1
- package/dist/SwipePivotTabBar-BrQismcZ.cjs +0 -2
- package/dist/SwipePivotTabBar-BrQismcZ.cjs.map +0 -1
- package/dist/useDocumentPointerEvents-CKdhGXd0.js +0 -46
- package/dist/useDocumentPointerEvents-CKdhGXd0.js.map +0 -1
- package/dist/useDocumentPointerEvents-ChqrKXDk.cjs +0 -2
- package/dist/useDocumentPointerEvents-ChqrKXDk.cjs.map +0 -1
- package/dist/useEffectEvent-Dp7HLCf0.js +0 -13
- package/dist/useEffectEvent-Dp7HLCf0.js.map +0 -1
- package/dist/useEffectEvent-huSsGUnl.cjs +0 -2
- 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
|
-
|
|
19
|
-
|
|
17
|
+
const rafState = {
|
|
18
|
+
callbacks: [] as FrameRequestCallback[],
|
|
19
|
+
id: 0,
|
|
20
|
+
originalRAF: globalThis.requestAnimationFrame,
|
|
21
|
+
originalCAF: globalThis.cancelAnimationFrame,
|
|
22
|
+
};
|
|
20
23
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
24
|
+
const resetRafState = (): void => {
|
|
25
|
+
rafState.callbacks = [];
|
|
26
|
+
rafState.id = 0;
|
|
27
|
+
};
|
|
25
28
|
|
|
26
|
-
const
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
vi.stubGlobal("cancelAnimationFrame", mockCAF);
|
|
62
|
+
resetRafState();
|
|
63
|
+
globalThis.requestAnimationFrame = mockRAF;
|
|
64
|
+
globalThis.cancelAnimationFrame = mockCAF;
|
|
41
65
|
});
|
|
42
66
|
|
|
43
67
|
afterEach(() => {
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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={
|
|
636
|
+
renderIndicator={indicatorTracker.fn}
|
|
622
637
|
/>
|
|
623
638
|
);
|
|
624
639
|
|
|
625
|
-
expect(
|
|
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
|
|
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={
|
|
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={
|
|
675
|
+
renderIndicator={indicatorTracker.fn}
|
|
658
676
|
/>
|
|
659
677
|
);
|
|
660
678
|
|
|
661
679
|
// Last call should have the swipe offset
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
758
|
-
|
|
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(
|
|
773
|
-
expect(
|
|
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
|
-
|
|
786
|
-
|
|
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(
|
|
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
|
-
|
|
808
|
-
let indicatorCenterX = 0;
|
|
823
|
+
const indicatorState = { offsetPx: 0, centerX: 0 };
|
|
809
824
|
|
|
810
825
|
// Mock performance.now to control animation timing
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
824
|
-
|
|
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(
|
|
847
|
+
expect(indicatorState.centerX).toBe(0);
|
|
832
848
|
// offsetPx includes active tab position
|
|
833
|
-
expect(
|
|
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
|
-
|
|
845
|
-
|
|
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
|
-
|
|
868
|
+
timeState.value = 500; // Advance past animation duration (300ms default)
|
|
853
869
|
act(() => {
|
|
854
|
-
|
|
870
|
+
Array.from({ length: 10 }).forEach(() => {
|
|
855
871
|
flushRAF();
|
|
856
|
-
}
|
|
872
|
+
});
|
|
857
873
|
});
|
|
858
874
|
|
|
859
875
|
// centerX stays fixed
|
|
860
|
-
expect(
|
|
876
|
+
expect(indicatorState.centerX).toBe(0);
|
|
861
877
|
// offsetPx now includes Tab 3 position (after animation completes)
|
|
862
|
-
expect(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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).
|
|
269
|
+
expect(onActiveChange.calls).toHaveLength(1);
|
|
270
|
+
expect(onActiveChange.calls[0]?.[0]).toBe("b");
|
|
257
271
|
});
|
|
258
272
|
});
|
|
259
273
|
|