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,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Hook for detecting swipe gestures to open/close a drawer.
|
|
3
|
+
*
|
|
4
|
+
* Combines:
|
|
5
|
+
* - Edge swipe detection for opening (Stack pattern)
|
|
6
|
+
* - Drag-to-close within drawer (Dialog pattern)
|
|
7
|
+
* - Native gesture guard for browser back prevention
|
|
8
|
+
*/
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
import { useEdgeSwipeInput } from "../../hooks/gesture/useEdgeSwipeInput.js";
|
|
11
|
+
import { useNativeGestureGuard } from "../../hooks/gesture/useNativeGestureGuard.js";
|
|
12
|
+
import { usePointerTracking } from "../../hooks/gesture/usePointerTracking.js";
|
|
13
|
+
import {
|
|
14
|
+
mergeGestureContainerProps,
|
|
15
|
+
isScrollableInDirection,
|
|
16
|
+
} from "../../hooks/gesture/utils.js";
|
|
17
|
+
import { isInSwipeSafeZone } from "../../components/gesture/SwipeSafeZone.js";
|
|
18
|
+
import {
|
|
19
|
+
type ContinuousOperationState,
|
|
20
|
+
IDLE_CONTINUOUS_OPERATION_STATE,
|
|
21
|
+
} from "../../hooks/gesture/types.js";
|
|
22
|
+
import type { UseDrawerSwipeInputOptions, UseDrawerSwipeInputResult } from "./types.js";
|
|
23
|
+
import { getDrawerAnimationAxis, getDrawerCloseSwipeSign, getDrawerOpenSwipeSign } from "./types.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Default dismiss threshold (30% of container size).
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_DISMISS_THRESHOLD = 0.3;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Velocity threshold for quick flick dismissal (px/ms).
|
|
32
|
+
*/
|
|
33
|
+
const VELOCITY_THRESHOLD = 0.5;
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Helper functions (extracted to avoid ternary violations)
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
function getContainerSize(container: HTMLElement, axis: "x" | "y"): number {
|
|
40
|
+
if (axis === "x") {
|
|
41
|
+
return container.clientWidth;
|
|
42
|
+
}
|
|
43
|
+
return container.clientHeight;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getAxisDelta(
|
|
47
|
+
start: { x: number; y: number },
|
|
48
|
+
current: { x: number; y: number },
|
|
49
|
+
axis: "x" | "y",
|
|
50
|
+
): number {
|
|
51
|
+
if (axis === "x") {
|
|
52
|
+
return current.x - start.x;
|
|
53
|
+
}
|
|
54
|
+
return current.y - start.y;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PHASE_MAP: Record<string, "idle" | "operating" | "ended"> = {
|
|
58
|
+
idle: "idle",
|
|
59
|
+
ended: "ended",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function normalizePhase(phase: string): "idle" | "operating" | "ended" {
|
|
63
|
+
return PHASE_MAP[phase] ?? "operating";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function computeDisplacementValue(
|
|
67
|
+
closeSwipeSign: 1 | -1,
|
|
68
|
+
axis: "x" | "y",
|
|
69
|
+
closeDisplacement: number,
|
|
70
|
+
): { x: number; y: number } {
|
|
71
|
+
const signedDisplacement = closeSwipeSign * closeDisplacement;
|
|
72
|
+
if (axis === "x") {
|
|
73
|
+
return { x: signedDisplacement, y: 0 };
|
|
74
|
+
}
|
|
75
|
+
return { x: 0, y: signedDisplacement };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function computeAxisDisplacement(
|
|
79
|
+
displacement: { x: number; y: number },
|
|
80
|
+
axis: "x" | "y",
|
|
81
|
+
): number {
|
|
82
|
+
if (axis === "x") {
|
|
83
|
+
return Math.abs(displacement.x);
|
|
84
|
+
}
|
|
85
|
+
return Math.abs(displacement.y);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isEdgeSwipeEnabled(enableEdgeSwipeOpen: boolean, isOpen: boolean): boolean {
|
|
89
|
+
if (!enableEdgeSwipeOpen) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return !isOpen;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isCloseSwipeEnabled(enableSwipeClose: boolean, isOpen: boolean): boolean {
|
|
96
|
+
if (!enableSwipeClose) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return isOpen;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isDrawerOpening(isEdgeGesture: boolean, isOpen: boolean): boolean {
|
|
103
|
+
if (!isEdgeGesture) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return !isOpen;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isDrawerClosing(closePhase: "idle" | "operating" | "ended", isOpen: boolean): boolean {
|
|
110
|
+
if (closePhase === "idle") {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return isOpen;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function computeVelocity(
|
|
117
|
+
start: { x: number; y: number; timestamp: number } | null,
|
|
118
|
+
current: { x: number; y: number; timestamp: number } | null,
|
|
119
|
+
displacement: number,
|
|
120
|
+
): number {
|
|
121
|
+
if (!start || !current) {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const timeDelta = Math.max(1, current.timestamp - start.timestamp);
|
|
125
|
+
return displacement / timeDelta;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Hook for detecting swipe gestures to open/close a drawer.
|
|
130
|
+
*
|
|
131
|
+
* When drawer is closed:
|
|
132
|
+
* - Detects edge swipe from the anchor edge to trigger open
|
|
133
|
+
*
|
|
134
|
+
* When drawer is open:
|
|
135
|
+
* - Detects drag gesture within drawer to trigger close
|
|
136
|
+
* - Respects scrollable content boundaries
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```tsx
|
|
140
|
+
* const { state, edgeContainerProps, drawerContentProps } = useDrawerSwipeInput({
|
|
141
|
+
* edgeContainerRef: gridLayoutRef,
|
|
142
|
+
* drawerContentRef: drawerRef,
|
|
143
|
+
* direction: "left",
|
|
144
|
+
* isOpen,
|
|
145
|
+
* onSwipeOpen: () => setOpen(true),
|
|
146
|
+
* onSwipeClose: () => setOpen(false),
|
|
147
|
+
* });
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function useDrawerSwipeInput(
|
|
151
|
+
options: UseDrawerSwipeInputOptions,
|
|
152
|
+
): UseDrawerSwipeInputResult {
|
|
153
|
+
const {
|
|
154
|
+
edgeContainerRef,
|
|
155
|
+
drawerContentRef,
|
|
156
|
+
direction,
|
|
157
|
+
isOpen,
|
|
158
|
+
onSwipeOpen,
|
|
159
|
+
onSwipeClose,
|
|
160
|
+
enableEdgeSwipeOpen = true,
|
|
161
|
+
enableSwipeClose = true,
|
|
162
|
+
edgeWidth = 20,
|
|
163
|
+
dismissThreshold = DEFAULT_DISMISS_THRESHOLD,
|
|
164
|
+
} = options;
|
|
165
|
+
|
|
166
|
+
const axis = getDrawerAnimationAxis(direction);
|
|
167
|
+
const closeSwipeSign = getDrawerCloseSwipeSign(direction);
|
|
168
|
+
const openSwipeSign = getDrawerOpenSwipeSign(direction);
|
|
169
|
+
|
|
170
|
+
// Track container size for progress calculation
|
|
171
|
+
const containerSizeRef = React.useRef(0);
|
|
172
|
+
|
|
173
|
+
// Measure drawer content size
|
|
174
|
+
React.useLayoutEffect(() => {
|
|
175
|
+
const container = drawerContentRef.current;
|
|
176
|
+
if (!container) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const updateSize = () => {
|
|
181
|
+
containerSizeRef.current = getContainerSize(container, axis);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
updateSize();
|
|
185
|
+
|
|
186
|
+
const observer = new ResizeObserver(updateSize);
|
|
187
|
+
observer.observe(container);
|
|
188
|
+
|
|
189
|
+
return () => observer.disconnect();
|
|
190
|
+
}, [drawerContentRef, axis]);
|
|
191
|
+
|
|
192
|
+
// =========== Edge swipe to OPEN ===========
|
|
193
|
+
const handleOpenSwipeEnd = React.useCallback(
|
|
194
|
+
(state: { direction: 1 | -1 | 0 }) => {
|
|
195
|
+
// Open when swiping in the correct direction (away from edge)
|
|
196
|
+
if (state.direction === openSwipeSign) {
|
|
197
|
+
onSwipeOpen();
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
[openSwipeSign, onSwipeOpen],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const {
|
|
204
|
+
isEdgeGesture,
|
|
205
|
+
state: edgeSwipeState,
|
|
206
|
+
containerProps: edgeSwipeProps,
|
|
207
|
+
} = useEdgeSwipeInput({
|
|
208
|
+
containerRef: edgeContainerRef,
|
|
209
|
+
edge: direction,
|
|
210
|
+
edgeWidth,
|
|
211
|
+
enabled: isEdgeSwipeEnabled(enableEdgeSwipeOpen, isOpen),
|
|
212
|
+
onSwipeEnd: handleOpenSwipeEnd,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Native gesture guard for edge swipe
|
|
216
|
+
const { containerProps: guardProps } = useNativeGestureGuard({
|
|
217
|
+
containerRef: edgeContainerRef,
|
|
218
|
+
active: isEdgeGesture,
|
|
219
|
+
preventEdgeBack: true,
|
|
220
|
+
preventOverscroll: true,
|
|
221
|
+
edgeWidth,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// =========== Drag to CLOSE (Dialog pattern) ===========
|
|
225
|
+
const { state: closeTracking, onPointerDown: baseClosePointerDown } = usePointerTracking({
|
|
226
|
+
enabled: isCloseSwipeEnabled(enableSwipeClose, isOpen),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const [closePhase, setClosePhase] = React.useState<"idle" | "operating" | "ended">("idle");
|
|
230
|
+
const lastCloseDisplacementRef = React.useRef(0);
|
|
231
|
+
|
|
232
|
+
// Wrap pointer down with scroll check and safe zone check
|
|
233
|
+
const onClosePointerDown = React.useCallback(
|
|
234
|
+
(event: React.PointerEvent) => {
|
|
235
|
+
if (!enableSwipeClose || !isOpen) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const container = drawerContentRef.current;
|
|
240
|
+
if (!container) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const target = event.target as HTMLElement;
|
|
245
|
+
|
|
246
|
+
// Check if target is in a SwipeSafeZone
|
|
247
|
+
if (isInSwipeSafeZone(target, container)) {
|
|
248
|
+
return; // Don't start close swipe if inside safe zone
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check if target is in a scrollable area that would block swipe
|
|
252
|
+
if (isScrollableInDirection(target, container, axis, closeSwipeSign)) {
|
|
253
|
+
return; // Don't start close swipe if inside scrollable content
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
baseClosePointerDown(event);
|
|
257
|
+
},
|
|
258
|
+
[enableSwipeClose, isOpen, drawerContentRef, axis, closeSwipeSign, baseClosePointerDown],
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Calculate close displacement
|
|
262
|
+
const closeDisplacement = React.useMemo(() => {
|
|
263
|
+
if (!closeTracking.isDown || !closeTracking.start || !closeTracking.current) {
|
|
264
|
+
return lastCloseDisplacementRef.current;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const delta = getAxisDelta(closeTracking.start, closeTracking.current, axis);
|
|
268
|
+
|
|
269
|
+
// Only count movement in close direction
|
|
270
|
+
const signedDelta = delta * closeSwipeSign;
|
|
271
|
+
return Math.max(0, signedDelta);
|
|
272
|
+
}, [closeTracking.isDown, closeTracking.start, closeTracking.current, axis, closeSwipeSign]);
|
|
273
|
+
|
|
274
|
+
// Track displacement while dragging
|
|
275
|
+
React.useEffect(() => {
|
|
276
|
+
if (closeTracking.isDown && closeTracking.current) {
|
|
277
|
+
lastCloseDisplacementRef.current = closeDisplacement;
|
|
278
|
+
}
|
|
279
|
+
}, [closeTracking.isDown, closeTracking.current, closeDisplacement]);
|
|
280
|
+
|
|
281
|
+
// Handle close drag start
|
|
282
|
+
React.useEffect(() => {
|
|
283
|
+
if (closeTracking.isDown && closePhase === "idle") {
|
|
284
|
+
setClosePhase("operating");
|
|
285
|
+
}
|
|
286
|
+
}, [closeTracking.isDown, closePhase]);
|
|
287
|
+
|
|
288
|
+
// Handle close drag end
|
|
289
|
+
React.useEffect(() => {
|
|
290
|
+
if (!closeTracking.isDown && closePhase === "operating") {
|
|
291
|
+
const displacement = lastCloseDisplacementRef.current;
|
|
292
|
+
const hasMovement = displacement > 1;
|
|
293
|
+
|
|
294
|
+
if (hasMovement) {
|
|
295
|
+
setClosePhase("ended");
|
|
296
|
+
|
|
297
|
+
// Check if should close
|
|
298
|
+
const containerSize = containerSizeRef.current;
|
|
299
|
+
if (containerSize > 0) {
|
|
300
|
+
const ratio = displacement / containerSize;
|
|
301
|
+
const velocity = computeVelocity(closeTracking.start, closeTracking.current, displacement);
|
|
302
|
+
|
|
303
|
+
if (ratio >= dismissThreshold || velocity >= VELOCITY_THRESHOLD) {
|
|
304
|
+
onSwipeClose();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
setClosePhase("idle");
|
|
309
|
+
lastCloseDisplacementRef.current = 0;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}, [closeTracking.isDown, closePhase, dismissThreshold, onSwipeClose, closeTracking.start, closeTracking.current]);
|
|
313
|
+
|
|
314
|
+
// Transition from ended to idle
|
|
315
|
+
React.useEffect(() => {
|
|
316
|
+
if (closePhase === "ended") {
|
|
317
|
+
queueMicrotask(() => {
|
|
318
|
+
setClosePhase("idle");
|
|
319
|
+
lastCloseDisplacementRef.current = 0;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}, [closePhase]);
|
|
323
|
+
|
|
324
|
+
// Reset close state when drawer closes
|
|
325
|
+
React.useEffect(() => {
|
|
326
|
+
if (!isOpen) {
|
|
327
|
+
setClosePhase("idle");
|
|
328
|
+
lastCloseDisplacementRef.current = 0;
|
|
329
|
+
}
|
|
330
|
+
}, [isOpen]);
|
|
331
|
+
|
|
332
|
+
// =========== Combined state ===========
|
|
333
|
+
const isOpening = isDrawerOpening(isEdgeGesture, isOpen);
|
|
334
|
+
const isClosing = isDrawerClosing(closePhase, isOpen);
|
|
335
|
+
|
|
336
|
+
// Determine primary displacement based on current operation
|
|
337
|
+
const displacement = React.useMemo(() => {
|
|
338
|
+
if (isOpening) {
|
|
339
|
+
return computeAxisDisplacement(edgeSwipeState.displacement, axis);
|
|
340
|
+
}
|
|
341
|
+
if (isClosing) {
|
|
342
|
+
return closeDisplacement;
|
|
343
|
+
}
|
|
344
|
+
return 0;
|
|
345
|
+
}, [isOpening, isClosing, axis, edgeSwipeState.displacement, closeDisplacement]);
|
|
346
|
+
|
|
347
|
+
// Progress calculation
|
|
348
|
+
const progress = React.useMemo(() => {
|
|
349
|
+
const containerSize = containerSizeRef.current;
|
|
350
|
+
if (containerSize <= 0) {
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
return Math.min(displacement / containerSize, 1);
|
|
354
|
+
}, [displacement]);
|
|
355
|
+
|
|
356
|
+
// Combined operation state
|
|
357
|
+
const state = React.useMemo<ContinuousOperationState>(() => {
|
|
358
|
+
if (isOpening) {
|
|
359
|
+
return {
|
|
360
|
+
phase: normalizePhase(edgeSwipeState.phase),
|
|
361
|
+
displacement: edgeSwipeState.displacement,
|
|
362
|
+
velocity: edgeSwipeState.velocity,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (isClosing) {
|
|
366
|
+
return {
|
|
367
|
+
phase: closePhase,
|
|
368
|
+
displacement: computeDisplacementValue(closeSwipeSign, axis, closeDisplacement),
|
|
369
|
+
velocity: { x: 0, y: 0 },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return IDLE_CONTINUOUS_OPERATION_STATE;
|
|
373
|
+
}, [isOpening, isClosing, edgeSwipeState, closePhase, closeDisplacement, axis, closeSwipeSign]);
|
|
374
|
+
|
|
375
|
+
// Container props
|
|
376
|
+
const edgeContainerProps = React.useMemo(
|
|
377
|
+
() => mergeGestureContainerProps(edgeSwipeProps, guardProps),
|
|
378
|
+
[edgeSwipeProps, guardProps],
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const drawerContentProps = React.useMemo(() => ({
|
|
382
|
+
onPointerDown: onClosePointerDown,
|
|
383
|
+
style: {
|
|
384
|
+
touchAction: "none" as const,
|
|
385
|
+
userSelect: "none" as const,
|
|
386
|
+
WebkitUserSelect: "none" as const,
|
|
387
|
+
},
|
|
388
|
+
}), [onClosePointerDown]);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
state,
|
|
392
|
+
isOpening,
|
|
393
|
+
isClosing,
|
|
394
|
+
progress,
|
|
395
|
+
displacement,
|
|
396
|
+
edgeContainerProps,
|
|
397
|
+
drawerContentProps,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file ContentRegistry tests - state persistence across tab switch, panel move, and split
|
|
3
3
|
*/
|
|
4
|
-
/* eslint-disable no-restricted-imports, no-restricted-properties, no-restricted-syntax -- integration test */
|
|
5
4
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
6
5
|
import * as React from "react";
|
|
7
6
|
import { ContentRegistryProvider, useContentRegistry } from "./ContentRegistry";
|
|
@@ -49,6 +48,21 @@ const TestHarness: React.FC<{ state: TestState }> = ({ state }) => {
|
|
|
49
48
|
};
|
|
50
49
|
|
|
51
50
|
describe("ContentRegistry", () => {
|
|
51
|
+
const defaultRect = {
|
|
52
|
+
top: 0,
|
|
53
|
+
left: 0,
|
|
54
|
+
width: 100,
|
|
55
|
+
height: 100,
|
|
56
|
+
right: 100,
|
|
57
|
+
bottom: 100,
|
|
58
|
+
x: 0,
|
|
59
|
+
y: 0,
|
|
60
|
+
toJSON: () => ({}),
|
|
61
|
+
} as DOMRect;
|
|
62
|
+
const originalPointerCapture = Element.prototype.setPointerCapture;
|
|
63
|
+
const originalReleasePointerCapture = Element.prototype.releasePointerCapture;
|
|
64
|
+
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
|
|
65
|
+
|
|
52
66
|
const createPanel = (id: string): TabDefinition => ({
|
|
53
67
|
id,
|
|
54
68
|
title: `Panel ${id}`,
|
|
@@ -59,25 +73,18 @@ describe("ContentRegistry", () => {
|
|
|
59
73
|
// Mock ResizeObserver (polyfill provided in vitest.setup.ts)
|
|
60
74
|
|
|
61
75
|
// Mock pointer capture methods
|
|
62
|
-
Element.prototype.setPointerCapture =
|
|
63
|
-
Element.prototype.releasePointerCapture =
|
|
76
|
+
Element.prototype.setPointerCapture = () => {};
|
|
77
|
+
Element.prototype.releasePointerCapture = () => {};
|
|
64
78
|
// Mock getBoundingClientRect
|
|
65
|
-
Element.prototype.getBoundingClientRect =
|
|
66
|
-
top: 0,
|
|
67
|
-
left: 0,
|
|
68
|
-
width: 100,
|
|
69
|
-
height: 100,
|
|
70
|
-
right: 100,
|
|
71
|
-
bottom: 100,
|
|
72
|
-
x: 0,
|
|
73
|
-
y: 0,
|
|
74
|
-
toJSON: () => ({}),
|
|
75
|
-
});
|
|
79
|
+
Element.prototype.getBoundingClientRect = () => defaultRect;
|
|
76
80
|
});
|
|
77
81
|
|
|
78
82
|
afterEach(() => {
|
|
79
83
|
// Clean up any portal containers
|
|
80
84
|
document.querySelectorAll("[data-panel-content-root]").forEach((el) => el.remove());
|
|
85
|
+
Element.prototype.setPointerCapture = originalPointerCapture;
|
|
86
|
+
Element.prototype.releasePointerCapture = originalReleasePointerCapture;
|
|
87
|
+
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
|
81
88
|
});
|
|
82
89
|
|
|
83
90
|
it("should render content inside the registered container element", () => {
|
|
@@ -31,8 +31,12 @@ const shouldRenderItem = (offset: number): boolean => {
|
|
|
31
31
|
* Helper to convert offset to display position
|
|
32
32
|
*/
|
|
33
33
|
const toDisplayPosition = (offset: number): -1 | 0 | 1 => {
|
|
34
|
-
if (offset < 0)
|
|
35
|
-
|
|
34
|
+
if (offset < 0) {
|
|
35
|
+
return -1;
|
|
36
|
+
}
|
|
37
|
+
if (offset > 0) {
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
36
40
|
return 0;
|
|
37
41
|
};
|
|
38
42
|
|
|
@@ -76,9 +80,9 @@ describe("SwipePivotContent position handling", () => {
|
|
|
76
80
|
const activeIndex = 0;
|
|
77
81
|
|
|
78
82
|
// Filter to only adjacent items BEFORE rendering
|
|
79
|
-
const itemsToRender = items.filter(item =>
|
|
80
|
-
shouldRenderItem(getPositionOffset(item.index, activeIndex))
|
|
81
|
-
);
|
|
83
|
+
const itemsToRender = items.filter((item) => {
|
|
84
|
+
return shouldRenderItem(getPositionOffset(item.index, activeIndex));
|
|
85
|
+
});
|
|
82
86
|
|
|
83
87
|
render(
|
|
84
88
|
<>
|
|
@@ -114,9 +118,9 @@ describe("SwipePivotContent position handling", () => {
|
|
|
114
118
|
];
|
|
115
119
|
const activeIndex = 1; // On Page 2
|
|
116
120
|
|
|
117
|
-
const itemsToRender = items.filter(item =>
|
|
118
|
-
shouldRenderItem(getPositionOffset(item.index, activeIndex))
|
|
119
|
-
);
|
|
121
|
+
const itemsToRender = items.filter((item) => {
|
|
122
|
+
return shouldRenderItem(getPositionOffset(item.index, activeIndex));
|
|
123
|
+
});
|
|
120
124
|
|
|
121
125
|
render(
|
|
122
126
|
<>
|
|
@@ -6,41 +6,71 @@ import * as React from "react";
|
|
|
6
6
|
import { SwipePivotContent } from "./SwipePivotContent.js";
|
|
7
7
|
import type { SwipeInputState } from "../../hooks/gesture/types.js";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
}
|
|
9
|
+
/**
|
|
10
|
+
* Mock Animation that implements the full Animation interface.
|
|
11
|
+
* Used to polyfill Web Animations API for JSDOM testing.
|
|
12
|
+
*/
|
|
13
|
+
const createMockAnimation = (): Animation => {
|
|
14
|
+
const animation = {} as Animation;
|
|
15
|
+
const animationState = {
|
|
16
|
+
resolveFinished: (value: Animation): void => {
|
|
17
|
+
void value;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
animation.currentTime = 0;
|
|
22
|
+
animation.effect = null;
|
|
23
|
+
animation.id = "";
|
|
24
|
+
animation.oncancel = null;
|
|
25
|
+
animation.onfinish = null;
|
|
26
|
+
animation.onremove = null;
|
|
27
|
+
animation.pending = false;
|
|
28
|
+
animation.playState = "running";
|
|
29
|
+
animation.playbackRate = 1;
|
|
30
|
+
animation.replaceState = "active";
|
|
31
|
+
animation.startTime = 0;
|
|
32
|
+
animation.timeline = null;
|
|
33
|
+
animation.finished = new Promise((resolve) => {
|
|
34
|
+
animationState.resolveFinished = resolve;
|
|
35
|
+
});
|
|
36
|
+
animation.ready = Promise.resolve(animation);
|
|
37
|
+
animation.cancel = () => {
|
|
38
|
+
animation.playState = "idle";
|
|
39
|
+
};
|
|
40
|
+
animation.finish = () => {
|
|
41
|
+
animation.playState = "finished";
|
|
42
|
+
animationState.resolveFinished(animation);
|
|
43
|
+
};
|
|
44
|
+
animation.commitStyles = () => {};
|
|
45
|
+
animation.pause = () => {};
|
|
46
|
+
animation.persist = () => {};
|
|
47
|
+
animation.play = () => {};
|
|
48
|
+
animation.reverse = () => {};
|
|
49
|
+
animation.updatePlaybackRate = () => {};
|
|
24
50
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
51
|
+
// EventTarget methods
|
|
52
|
+
animation.addEventListener = () => {};
|
|
53
|
+
animation.removeEventListener = () => {};
|
|
54
|
+
animation.dispatchEvent = () => true;
|
|
55
|
+
|
|
56
|
+
return animation;
|
|
57
|
+
};
|
|
30
58
|
|
|
31
59
|
describe("SwipePivotContent", () => {
|
|
32
|
-
|
|
60
|
+
const animationState = {
|
|
61
|
+
originalAnimate: Element.prototype.animate as typeof Element.prototype.animate | undefined,
|
|
62
|
+
};
|
|
33
63
|
|
|
34
64
|
beforeAll(() => {
|
|
35
|
-
originalAnimate = Element.prototype.animate;
|
|
36
|
-
Element.prototype.animate =
|
|
37
|
-
return
|
|
65
|
+
animationState.originalAnimate = Element.prototype.animate;
|
|
66
|
+
Element.prototype.animate = (): Animation => {
|
|
67
|
+
return createMockAnimation();
|
|
38
68
|
};
|
|
39
69
|
});
|
|
40
70
|
|
|
41
71
|
afterAll(() => {
|
|
42
|
-
if (originalAnimate) {
|
|
43
|
-
Element.prototype.animate = originalAnimate;
|
|
72
|
+
if (animationState.originalAnimate) {
|
|
73
|
+
Element.prototype.animate = animationState.originalAnimate;
|
|
44
74
|
}
|
|
45
75
|
});
|
|
46
76
|
const idleState: SwipeInputState = {
|
|
@@ -140,14 +140,14 @@ export const SwipePivotContent: React.FC<SwipePivotContentProps> = React.memo(({
|
|
|
140
140
|
|
|
141
141
|
const targetPx = position * containerSize;
|
|
142
142
|
const displacement = getAxisDisplacement(inputState, axis);
|
|
143
|
-
const
|
|
143
|
+
const isOperating = inputState.phase === "swiping" || inputState.phase === "tracking";
|
|
144
144
|
|
|
145
145
|
// Use shared transform hook for DOM manipulation
|
|
146
146
|
const { animationDirection } = useSwipeContentTransform({
|
|
147
147
|
elementRef,
|
|
148
148
|
targetPx,
|
|
149
149
|
displacement,
|
|
150
|
-
|
|
150
|
+
isOperating,
|
|
151
151
|
axis,
|
|
152
152
|
animationDuration,
|
|
153
153
|
containerSize,
|