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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tests for useDialogSwipeInput hook
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
5
|
+
import { renderHook, act } from "@testing-library/react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { useDialogSwipeInput } from "./useDialogSwipeInput.js";
|
|
8
|
+
|
|
9
|
+
// Mock ResizeObserver
|
|
10
|
+
vi.stubGlobal(
|
|
11
|
+
"ResizeObserver",
|
|
12
|
+
class {
|
|
13
|
+
observe = vi.fn();
|
|
14
|
+
unobserve = vi.fn();
|
|
15
|
+
disconnect = vi.fn();
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
describe("useDialogSwipeInput", () => {
|
|
20
|
+
const createMockContainer = (dimensions: { width: number; height: number }) => {
|
|
21
|
+
const container = document.createElement("div");
|
|
22
|
+
Object.defineProperty(container, "clientWidth", { value: dimensions.width });
|
|
23
|
+
Object.defineProperty(container, "clientHeight", { value: dimensions.height });
|
|
24
|
+
return container;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.useFakeTimers();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.useRealTimers();
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("initialization", () => {
|
|
37
|
+
it("should return idle state initially", () => {
|
|
38
|
+
const container = createMockContainer({ width: 400, height: 300 });
|
|
39
|
+
const containerRef = { current: container };
|
|
40
|
+
|
|
41
|
+
const { result } = renderHook(() =>
|
|
42
|
+
useDialogSwipeInput({
|
|
43
|
+
containerRef,
|
|
44
|
+
openDirection: "bottom",
|
|
45
|
+
enabled: true,
|
|
46
|
+
onSwipeDismiss: vi.fn(),
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(result.current.state.phase).toBe("idle");
|
|
51
|
+
expect(result.current.isOperating).toBe(false);
|
|
52
|
+
expect(result.current.displacement).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should provide containerProps with touch-action style", () => {
|
|
56
|
+
const container = createMockContainer({ width: 400, height: 300 });
|
|
57
|
+
const containerRef = { current: container };
|
|
58
|
+
|
|
59
|
+
const { result } = renderHook(() =>
|
|
60
|
+
useDialogSwipeInput({
|
|
61
|
+
containerRef,
|
|
62
|
+
openDirection: "bottom",
|
|
63
|
+
enabled: true,
|
|
64
|
+
onSwipeDismiss: vi.fn(),
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(result.current.containerProps.style).toBeDefined();
|
|
69
|
+
expect(result.current.containerProps.style.touchAction).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("free 2D movement", () => {
|
|
74
|
+
it("should use touch-action none for free movement", () => {
|
|
75
|
+
const container = createMockContainer({ width: 400, height: 300 });
|
|
76
|
+
const containerRef = { current: container };
|
|
77
|
+
|
|
78
|
+
const { result } = renderHook(() =>
|
|
79
|
+
useDialogSwipeInput({
|
|
80
|
+
containerRef,
|
|
81
|
+
openDirection: "bottom",
|
|
82
|
+
enabled: true,
|
|
83
|
+
onSwipeDismiss: vi.fn(),
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Free 2D movement requires touch-action: none
|
|
88
|
+
expect(result.current.containerProps.style.touchAction).toBe("none");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should provide displacement2D for 2D transform", () => {
|
|
92
|
+
const container = createMockContainer({ width: 400, height: 300 });
|
|
93
|
+
const containerRef = { current: container };
|
|
94
|
+
|
|
95
|
+
const { result } = renderHook(() =>
|
|
96
|
+
useDialogSwipeInput({
|
|
97
|
+
containerRef,
|
|
98
|
+
openDirection: "left",
|
|
99
|
+
enabled: true,
|
|
100
|
+
onSwipeDismiss: vi.fn(),
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Should provide 2D displacement
|
|
105
|
+
expect(result.current.displacement2D).toEqual({ x: 0, y: 0 });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("enabled state", () => {
|
|
110
|
+
it("should not respond to pointer events when disabled", () => {
|
|
111
|
+
const container = createMockContainer({ width: 400, height: 300 });
|
|
112
|
+
const containerRef = { current: container };
|
|
113
|
+
|
|
114
|
+
const { result } = renderHook(() =>
|
|
115
|
+
useDialogSwipeInput({
|
|
116
|
+
containerRef,
|
|
117
|
+
openDirection: "bottom",
|
|
118
|
+
enabled: false,
|
|
119
|
+
onSwipeDismiss: vi.fn(),
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Try to trigger pointer down
|
|
124
|
+
const pointerEvent = new PointerEvent("pointerdown", {
|
|
125
|
+
pointerId: 1,
|
|
126
|
+
clientX: 100,
|
|
127
|
+
clientY: 100,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
act(() => {
|
|
131
|
+
result.current.containerProps.onPointerDown?.(
|
|
132
|
+
pointerEvent as unknown as React.PointerEvent,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result.current.state.phase).toBe("idle");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("progress calculation", () => {
|
|
141
|
+
it("should return 0 progress when not swiping", () => {
|
|
142
|
+
const container = createMockContainer({ width: 400, height: 300 });
|
|
143
|
+
const containerRef = { current: container };
|
|
144
|
+
|
|
145
|
+
const { result } = renderHook(() =>
|
|
146
|
+
useDialogSwipeInput({
|
|
147
|
+
containerRef,
|
|
148
|
+
openDirection: "bottom",
|
|
149
|
+
enabled: true,
|
|
150
|
+
onSwipeDismiss: vi.fn(),
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(result.current.progress).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Hook for detecting swipe gestures to dismiss a dialog.
|
|
3
|
+
*
|
|
4
|
+
* This hook provides free 2D movement during swipe:
|
|
5
|
+
* - User can drag in any direction freely
|
|
6
|
+
* - Close intent is detected on release based on displacement direction
|
|
7
|
+
* - If movement matches close direction and exceeds threshold, dismiss
|
|
8
|
+
* - Otherwise, snap back to original position
|
|
9
|
+
*/
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import { usePointerTracking } from "../../hooks/gesture/usePointerTracking.js";
|
|
12
|
+
import {
|
|
13
|
+
type ContinuousOperationState,
|
|
14
|
+
type Vector2,
|
|
15
|
+
IDLE_CONTINUOUS_OPERATION_STATE,
|
|
16
|
+
} from "../../hooks/gesture/types.js";
|
|
17
|
+
import type { DialogOpenDirection } from "./dialogAnimationUtils.js";
|
|
18
|
+
import { getAnimationAxis, getDirectionSign } from "./dialogAnimationUtils.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default dismiss threshold (30% of container size).
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_DISMISS_THRESHOLD = 0.3;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Velocity threshold for quick flick dismissal (px/ms).
|
|
27
|
+
*/
|
|
28
|
+
const VELOCITY_THRESHOLD = 0.5;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for useDialogSwipeInput hook.
|
|
32
|
+
*/
|
|
33
|
+
export type UseDialogSwipeInputOptions = {
|
|
34
|
+
/** Ref to the dialog container element */
|
|
35
|
+
containerRef: React.RefObject<HTMLElement | null>;
|
|
36
|
+
/** Direction the dialog opened from (swipe closes in same direction) */
|
|
37
|
+
openDirection: DialogOpenDirection;
|
|
38
|
+
/** Whether swipe dismiss is enabled */
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
/** Callback when swipe exceeds threshold and dialog should dismiss */
|
|
41
|
+
onSwipeDismiss: () => void;
|
|
42
|
+
/** Threshold ratio (0-1) of container size to trigger dismiss. @default 0.3 */
|
|
43
|
+
dismissThreshold?: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Result from useDialogSwipeInput hook.
|
|
48
|
+
*/
|
|
49
|
+
export type UseDialogSwipeInputResult = {
|
|
50
|
+
/** Current operation state (idle, operating, or ended) */
|
|
51
|
+
state: ContinuousOperationState;
|
|
52
|
+
/** Props to spread on the container element */
|
|
53
|
+
containerProps: React.HTMLAttributes<HTMLElement> & {
|
|
54
|
+
style: React.CSSProperties;
|
|
55
|
+
};
|
|
56
|
+
/** Swipe progress (0-1) towards dismiss threshold */
|
|
57
|
+
progress: number;
|
|
58
|
+
/** Whether user is currently swiping */
|
|
59
|
+
isOperating: boolean;
|
|
60
|
+
/** Current displacement in pixels (primary axis based on openDirection) */
|
|
61
|
+
displacement: number;
|
|
62
|
+
/** Full 2D displacement for free movement transform */
|
|
63
|
+
displacement2D: Vector2;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if an element or its ancestors are scrollable in any direction.
|
|
68
|
+
*/
|
|
69
|
+
function isScrollableElement(element: HTMLElement): boolean {
|
|
70
|
+
const style = getComputedStyle(element);
|
|
71
|
+
|
|
72
|
+
const isScrollableX =
|
|
73
|
+
(style.overflowX === "scroll" || style.overflowX === "auto") &&
|
|
74
|
+
element.scrollWidth > element.clientWidth;
|
|
75
|
+
|
|
76
|
+
const isScrollableY =
|
|
77
|
+
(style.overflowY === "scroll" || style.overflowY === "auto") &&
|
|
78
|
+
element.scrollHeight > element.clientHeight;
|
|
79
|
+
|
|
80
|
+
return isScrollableX || isScrollableY;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if we should start tracking based on scroll state.
|
|
85
|
+
* Returns false if the target is inside a scrollable element that can scroll in the drag direction.
|
|
86
|
+
*/
|
|
87
|
+
function shouldStartDrag(event: React.PointerEvent, container: HTMLElement): boolean {
|
|
88
|
+
// eslint-disable-next-line no-restricted-syntax -- loop variable requires let
|
|
89
|
+
let current = event.target as HTMLElement | null;
|
|
90
|
+
|
|
91
|
+
while (current !== null && current !== container) {
|
|
92
|
+
if (isScrollableElement(current)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
current = current.parentElement;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Hook for detecting swipe gestures to dismiss a dialog.
|
|
103
|
+
*
|
|
104
|
+
* Allows free 2D movement - user can drag in any direction.
|
|
105
|
+
* On release, detects if the movement matches the close direction:
|
|
106
|
+
* - "center" or "bottom": close if moved down significantly
|
|
107
|
+
* - "top": close if moved up significantly
|
|
108
|
+
* - "left": close if moved left significantly
|
|
109
|
+
* - "right": close if moved right significantly
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```tsx
|
|
113
|
+
* const { state, containerProps, displacement2D } = useDialogSwipeInput({
|
|
114
|
+
* containerRef,
|
|
115
|
+
* openDirection: "bottom",
|
|
116
|
+
* enabled: true,
|
|
117
|
+
* onSwipeDismiss: () => setVisible(false),
|
|
118
|
+
* });
|
|
119
|
+
*
|
|
120
|
+
* // Apply 2D transform for free movement
|
|
121
|
+
* style={{ transform: `translate(${displacement2D.x}px, ${displacement2D.y}px)` }}
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export function useDialogSwipeInput(
|
|
125
|
+
options: UseDialogSwipeInputOptions,
|
|
126
|
+
): UseDialogSwipeInputResult {
|
|
127
|
+
const {
|
|
128
|
+
containerRef,
|
|
129
|
+
openDirection,
|
|
130
|
+
enabled,
|
|
131
|
+
onSwipeDismiss,
|
|
132
|
+
dismissThreshold = DEFAULT_DISMISS_THRESHOLD,
|
|
133
|
+
} = options;
|
|
134
|
+
|
|
135
|
+
const axis = getAnimationAxis(openDirection);
|
|
136
|
+
const expectedSign = getDirectionSign(openDirection);
|
|
137
|
+
|
|
138
|
+
// Track container size for progress calculation
|
|
139
|
+
const containerSizeRef = React.useRef<{ width: number; height: number }>({ width: 0, height: 0 });
|
|
140
|
+
|
|
141
|
+
React.useLayoutEffect(() => {
|
|
142
|
+
const container = containerRef.current;
|
|
143
|
+
if (!container) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const updateSize = () => {
|
|
148
|
+
containerSizeRef.current = {
|
|
149
|
+
width: container.clientWidth,
|
|
150
|
+
height: container.clientHeight,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
updateSize();
|
|
155
|
+
|
|
156
|
+
const observer = new ResizeObserver(updateSize);
|
|
157
|
+
observer.observe(container);
|
|
158
|
+
|
|
159
|
+
return () => observer.disconnect();
|
|
160
|
+
}, [containerRef]);
|
|
161
|
+
|
|
162
|
+
// Use pointer tracking for free 2D movement
|
|
163
|
+
const { state: tracking, onPointerDown: baseOnPointerDown } = usePointerTracking({
|
|
164
|
+
enabled,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Track displacement for snapback and release detection
|
|
168
|
+
const lastDisplacementRef = React.useRef<Vector2>({ x: 0, y: 0 });
|
|
169
|
+
|
|
170
|
+
// Wrap pointer down with scrollable check
|
|
171
|
+
const onPointerDown = React.useCallback(
|
|
172
|
+
(event: React.PointerEvent) => {
|
|
173
|
+
if (!enabled) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const container = containerRef.current;
|
|
177
|
+
if (!container) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!shouldStartDrag(event, container)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
baseOnPointerDown(event);
|
|
184
|
+
},
|
|
185
|
+
[enabled, containerRef, baseOnPointerDown],
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Calculate 2D displacement - preserve last value on release for snapback
|
|
189
|
+
const displacement2D = React.useMemo<Vector2>(() => {
|
|
190
|
+
if (!tracking.isDown || !tracking.start || !tracking.current) {
|
|
191
|
+
// Return last known displacement for snapback animation
|
|
192
|
+
return lastDisplacementRef.current;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
x: tracking.current.x - tracking.start.x,
|
|
196
|
+
y: tracking.current.y - tracking.start.y,
|
|
197
|
+
};
|
|
198
|
+
}, [tracking.isDown, tracking.start, tracking.current]);
|
|
199
|
+
|
|
200
|
+
// Calculate primary axis displacement for progress
|
|
201
|
+
const primaryDisplacement = axis === "x" ? displacement2D.x : displacement2D.y;
|
|
202
|
+
|
|
203
|
+
// Calculate progress towards dismiss threshold (only for correct direction)
|
|
204
|
+
const progress = React.useMemo(() => {
|
|
205
|
+
const containerSize = axis === "x" ? containerSizeRef.current.width : containerSizeRef.current.height;
|
|
206
|
+
if (containerSize <= 0) {
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
const sign = primaryDisplacement > 0 ? 1 : primaryDisplacement < 0 ? -1 : 0;
|
|
210
|
+
if (sign !== expectedSign) {
|
|
211
|
+
return 0; // Wrong direction
|
|
212
|
+
}
|
|
213
|
+
return Math.min(Math.abs(primaryDisplacement) / containerSize, 1);
|
|
214
|
+
}, [axis, primaryDisplacement, expectedSign]);
|
|
215
|
+
|
|
216
|
+
// State machine for operation phase
|
|
217
|
+
const [operationPhase, setOperationPhase] = React.useState<"idle" | "operating" | "ended">("idle");
|
|
218
|
+
|
|
219
|
+
// Store displacement while dragging
|
|
220
|
+
React.useEffect(() => {
|
|
221
|
+
if (tracking.isDown && tracking.current) {
|
|
222
|
+
lastDisplacementRef.current = displacement2D;
|
|
223
|
+
}
|
|
224
|
+
}, [tracking.isDown, tracking.current, displacement2D]);
|
|
225
|
+
|
|
226
|
+
// Handle drag start
|
|
227
|
+
React.useEffect(() => {
|
|
228
|
+
if (tracking.isDown && operationPhase === "idle") {
|
|
229
|
+
setOperationPhase("operating");
|
|
230
|
+
}
|
|
231
|
+
}, [tracking.isDown, operationPhase]);
|
|
232
|
+
|
|
233
|
+
// Handle release - transition to "ended" then check dismiss
|
|
234
|
+
React.useEffect(() => {
|
|
235
|
+
if (!tracking.isDown && operationPhase === "operating") {
|
|
236
|
+
const hasMovement = Math.abs(lastDisplacementRef.current.x) > 1 || Math.abs(lastDisplacementRef.current.y) > 1;
|
|
237
|
+
|
|
238
|
+
if (hasMovement) {
|
|
239
|
+
// Transition to ended phase for snapback detection
|
|
240
|
+
setOperationPhase("ended");
|
|
241
|
+
|
|
242
|
+
// Check if should dismiss
|
|
243
|
+
const containerSize = axis === "x" ? containerSizeRef.current.width : containerSizeRef.current.height;
|
|
244
|
+
if (containerSize > 0) {
|
|
245
|
+
const finalDisplacement = lastDisplacementRef.current;
|
|
246
|
+
const primaryValue = axis === "x" ? finalDisplacement.x : finalDisplacement.y;
|
|
247
|
+
const sign = primaryValue > 0 ? 1 : primaryValue < 0 ? -1 : 0;
|
|
248
|
+
|
|
249
|
+
if (sign === expectedSign) {
|
|
250
|
+
const ratio = Math.abs(primaryValue) / containerSize;
|
|
251
|
+
const velocity = tracking.start && tracking.current
|
|
252
|
+
? Math.abs(primaryValue) / Math.max(1, tracking.current.timestamp - tracking.start.timestamp)
|
|
253
|
+
: 0;
|
|
254
|
+
|
|
255
|
+
if (ratio >= dismissThreshold || velocity >= VELOCITY_THRESHOLD) {
|
|
256
|
+
onSwipeDismiss();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
// No significant movement, go directly to idle
|
|
262
|
+
setOperationPhase("idle");
|
|
263
|
+
lastDisplacementRef.current = { x: 0, y: 0 };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}, [tracking.isDown, operationPhase, axis, expectedSign, dismissThreshold, onSwipeDismiss, tracking.start, tracking.current]);
|
|
267
|
+
|
|
268
|
+
// Transition from ended to idle after one render (to allow snapback detection)
|
|
269
|
+
React.useEffect(() => {
|
|
270
|
+
if (operationPhase === "ended") {
|
|
271
|
+
// Use microtask to ensure the "ended" state is seen by consumers
|
|
272
|
+
queueMicrotask(() => {
|
|
273
|
+
setOperationPhase("idle");
|
|
274
|
+
lastDisplacementRef.current = { x: 0, y: 0 };
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}, [operationPhase]);
|
|
278
|
+
|
|
279
|
+
// Build continuous operation state
|
|
280
|
+
const state = React.useMemo<ContinuousOperationState>(() => {
|
|
281
|
+
if (operationPhase === "idle") {
|
|
282
|
+
return IDLE_CONTINUOUS_OPERATION_STATE;
|
|
283
|
+
}
|
|
284
|
+
if (operationPhase === "ended") {
|
|
285
|
+
return {
|
|
286
|
+
phase: "ended",
|
|
287
|
+
displacement: lastDisplacementRef.current,
|
|
288
|
+
velocity: { x: 0, y: 0 },
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
phase: "operating",
|
|
293
|
+
displacement: displacement2D,
|
|
294
|
+
velocity: { x: 0, y: 0 },
|
|
295
|
+
};
|
|
296
|
+
}, [operationPhase, displacement2D]);
|
|
297
|
+
|
|
298
|
+
const containerProps = React.useMemo(() => {
|
|
299
|
+
return {
|
|
300
|
+
onPointerDown,
|
|
301
|
+
style: {
|
|
302
|
+
touchAction: "none" as const, // Allow free 2D movement
|
|
303
|
+
userSelect: "none" as const,
|
|
304
|
+
WebkitUserSelect: "none" as const,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}, [onPointerDown]);
|
|
308
|
+
|
|
309
|
+
const isOperating = state.phase === "operating";
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
state,
|
|
313
|
+
containerProps,
|
|
314
|
+
progress,
|
|
315
|
+
isOperating,
|
|
316
|
+
displacement: primaryDisplacement,
|
|
317
|
+
displacement2D,
|
|
318
|
+
};
|
|
319
|
+
}
|