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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AlertDialog component for alert, confirm, and prompt dialogs
|
|
3
|
+
*/
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import type { AlertDialogProps } from "./types";
|
|
6
|
+
import { DialogContainer } from "./DialogContainer";
|
|
7
|
+
import {
|
|
8
|
+
FloatingPanelFrame,
|
|
9
|
+
FloatingPanelHeader,
|
|
10
|
+
FloatingPanelTitle,
|
|
11
|
+
FloatingPanelContent,
|
|
12
|
+
} from "../../components/paneling/FloatingPanelFrame";
|
|
13
|
+
import {
|
|
14
|
+
ALERT_DIALOG_WIDTH,
|
|
15
|
+
ALERT_DIALOG_BUTTON_GAP,
|
|
16
|
+
ALERT_DIALOG_ACTIONS_PADDING,
|
|
17
|
+
ALERT_DIALOG_MESSAGE_PADDING,
|
|
18
|
+
ALERT_DIALOG_INPUT_MARGIN_TOP,
|
|
19
|
+
FLOATING_PANEL_HEADER_PADDING_Y,
|
|
20
|
+
FLOATING_PANEL_HEADER_PADDING_X,
|
|
21
|
+
COLOR_PRIMARY,
|
|
22
|
+
COLOR_NODE_EDITOR_BORDER,
|
|
23
|
+
} from "../../constants/styles";
|
|
24
|
+
|
|
25
|
+
const alertDialogStyle: React.CSSProperties = {
|
|
26
|
+
width: ALERT_DIALOG_WIDTH,
|
|
27
|
+
maxWidth: "90vw",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const messageStyle: React.CSSProperties = {
|
|
31
|
+
padding: ALERT_DIALOG_MESSAGE_PADDING,
|
|
32
|
+
whiteSpace: "pre-wrap",
|
|
33
|
+
wordBreak: "break-word",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const actionsStyle: React.CSSProperties = {
|
|
37
|
+
display: "flex",
|
|
38
|
+
justifyContent: "flex-end",
|
|
39
|
+
gap: ALERT_DIALOG_BUTTON_GAP,
|
|
40
|
+
padding: ALERT_DIALOG_ACTIONS_PADDING,
|
|
41
|
+
borderTop: `1px solid ${COLOR_NODE_EDITOR_BORDER}`,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const inputStyle: React.CSSProperties = {
|
|
45
|
+
width: "100%",
|
|
46
|
+
padding: "8px 12px",
|
|
47
|
+
marginTop: ALERT_DIALOG_INPUT_MARGIN_TOP,
|
|
48
|
+
border: `1px solid ${COLOR_NODE_EDITOR_BORDER}`,
|
|
49
|
+
borderRadius: "4px",
|
|
50
|
+
fontSize: "14px",
|
|
51
|
+
boxSizing: "border-box",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const buttonBaseStyle: React.CSSProperties = {
|
|
55
|
+
padding: "8px 16px",
|
|
56
|
+
borderRadius: "4px",
|
|
57
|
+
fontSize: "14px",
|
|
58
|
+
fontWeight: 500,
|
|
59
|
+
cursor: "pointer",
|
|
60
|
+
border: "none",
|
|
61
|
+
transition: "background-color 0.15s ease",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const primaryButtonStyle: React.CSSProperties = {
|
|
65
|
+
...buttonBaseStyle,
|
|
66
|
+
backgroundColor: COLOR_PRIMARY,
|
|
67
|
+
color: "#fff",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const secondaryButtonStyle: React.CSSProperties = {
|
|
71
|
+
...buttonBaseStyle,
|
|
72
|
+
backgroundColor: "transparent",
|
|
73
|
+
border: `1px solid ${COLOR_NODE_EDITOR_BORDER}`,
|
|
74
|
+
color: "inherit",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Internal component for alert, confirm, and prompt dialogs
|
|
79
|
+
*/
|
|
80
|
+
export const AlertDialog: React.FC<AlertDialogProps> = ({
|
|
81
|
+
type,
|
|
82
|
+
visible,
|
|
83
|
+
title,
|
|
84
|
+
message,
|
|
85
|
+
confirmLabel = "OK",
|
|
86
|
+
cancelLabel = "Cancel",
|
|
87
|
+
placeholder,
|
|
88
|
+
defaultValue = "",
|
|
89
|
+
inputType = "text",
|
|
90
|
+
onConfirm,
|
|
91
|
+
onCancel,
|
|
92
|
+
swipeDismissible,
|
|
93
|
+
}) => {
|
|
94
|
+
// Default swipeDismissible: false for alert (shouldn't be accidentally dismissed), true otherwise
|
|
95
|
+
const effectiveSwipeDismissible = swipeDismissible ?? (type !== "alert");
|
|
96
|
+
const [inputValue, setInputValue] = React.useState(defaultValue);
|
|
97
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
98
|
+
|
|
99
|
+
// Reset input value when dialog opens
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
if (visible) {
|
|
102
|
+
setInputValue(defaultValue);
|
|
103
|
+
}
|
|
104
|
+
}, [visible, defaultValue]);
|
|
105
|
+
|
|
106
|
+
// Focus input when prompt dialog opens
|
|
107
|
+
React.useEffect(() => {
|
|
108
|
+
if (!visible) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (type !== "prompt") {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!inputRef.current) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Small delay to ensure dialog is rendered
|
|
118
|
+
const timeoutId = setTimeout(() => {
|
|
119
|
+
inputRef.current?.focus();
|
|
120
|
+
inputRef.current?.select();
|
|
121
|
+
}, 50);
|
|
122
|
+
return () => clearTimeout(timeoutId);
|
|
123
|
+
}, [visible, type]);
|
|
124
|
+
|
|
125
|
+
const handleConfirm = React.useCallback(() => {
|
|
126
|
+
if (type === "prompt") {
|
|
127
|
+
onConfirm(inputValue);
|
|
128
|
+
} else {
|
|
129
|
+
onConfirm();
|
|
130
|
+
}
|
|
131
|
+
}, [type, inputValue, onConfirm]);
|
|
132
|
+
|
|
133
|
+
const handleInputChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
134
|
+
setInputValue(event.target.value);
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
const handleInputKeyDown = React.useCallback(
|
|
138
|
+
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
139
|
+
if (event.key === "Enter") {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
handleConfirm();
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
[handleConfirm],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Prevent input events from bubbling up
|
|
148
|
+
const handleInputPointerDown = React.useCallback((event: React.PointerEvent) => {
|
|
149
|
+
event.stopPropagation();
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
const computeDialogLabel = (): string => {
|
|
153
|
+
if (title) {
|
|
154
|
+
return title;
|
|
155
|
+
}
|
|
156
|
+
if (type === "prompt") {
|
|
157
|
+
return "Prompt";
|
|
158
|
+
}
|
|
159
|
+
if (type === "confirm") {
|
|
160
|
+
return "Confirm";
|
|
161
|
+
}
|
|
162
|
+
return "Alert";
|
|
163
|
+
};
|
|
164
|
+
const dialogLabel = computeDialogLabel();
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<DialogContainer
|
|
168
|
+
visible={visible}
|
|
169
|
+
onClose={onCancel}
|
|
170
|
+
position="center"
|
|
171
|
+
dismissible={type !== "alert"}
|
|
172
|
+
closeOnEscape={true}
|
|
173
|
+
ariaLabel={dialogLabel}
|
|
174
|
+
swipeDismissible={effectiveSwipeDismissible}
|
|
175
|
+
>
|
|
176
|
+
<div style={alertDialogStyle}>
|
|
177
|
+
<FloatingPanelFrame>
|
|
178
|
+
<React.Activity mode={title ? "visible" : "hidden"}>
|
|
179
|
+
<FloatingPanelHeader
|
|
180
|
+
style={{
|
|
181
|
+
padding: `${FLOATING_PANEL_HEADER_PADDING_Y} ${FLOATING_PANEL_HEADER_PADDING_X}`,
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
<FloatingPanelTitle>{title}</FloatingPanelTitle>
|
|
185
|
+
</FloatingPanelHeader>
|
|
186
|
+
</React.Activity>
|
|
187
|
+
<FloatingPanelContent style={{ padding: 0 }}>
|
|
188
|
+
<div style={messageStyle}>
|
|
189
|
+
{message}
|
|
190
|
+
<React.Activity mode={type === "prompt" ? "visible" : "hidden"}>
|
|
191
|
+
<input
|
|
192
|
+
ref={inputRef}
|
|
193
|
+
type={inputType}
|
|
194
|
+
value={inputValue}
|
|
195
|
+
onChange={handleInputChange}
|
|
196
|
+
onKeyDown={handleInputKeyDown}
|
|
197
|
+
onPointerDown={handleInputPointerDown}
|
|
198
|
+
placeholder={placeholder}
|
|
199
|
+
style={inputStyle}
|
|
200
|
+
aria-label={placeholder ?? "Input"}
|
|
201
|
+
/>
|
|
202
|
+
</React.Activity>
|
|
203
|
+
</div>
|
|
204
|
+
<div style={actionsStyle}>
|
|
205
|
+
<React.Activity mode={type !== "alert" ? "visible" : "hidden"}>
|
|
206
|
+
<button type="button" style={secondaryButtonStyle} onClick={onCancel}>
|
|
207
|
+
{cancelLabel}
|
|
208
|
+
</button>
|
|
209
|
+
</React.Activity>
|
|
210
|
+
<button type="button" style={primaryButtonStyle} onClick={handleConfirm}>
|
|
211
|
+
{confirmLabel}
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
</FloatingPanelContent>
|
|
215
|
+
</FloatingPanelFrame>
|
|
216
|
+
</div>
|
|
217
|
+
</DialogContainer>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
AlertDialog.displayName = "AlertDialog";
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tests for DialogContainer component
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
5
|
+
import { DialogContainer } from "./DialogContainer";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
|
|
8
|
+
type CallTracker = {
|
|
9
|
+
calls: ReadonlyArray<ReadonlyArray<unknown>>;
|
|
10
|
+
fn: (...args: ReadonlyArray<unknown>) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const createCallTracker = (): CallTracker => {
|
|
14
|
+
const calls: Array<ReadonlyArray<unknown>> = [];
|
|
15
|
+
const fn = (...args: ReadonlyArray<unknown>): void => {
|
|
16
|
+
calls.push(args);
|
|
17
|
+
};
|
|
18
|
+
return { calls, fn };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("DialogContainer", () => {
|
|
22
|
+
const originalShowModal = HTMLDialogElement.prototype.showModal;
|
|
23
|
+
const originalClose = HTMLDialogElement.prototype.close;
|
|
24
|
+
const dialogCallState = {
|
|
25
|
+
showModal: createCallTracker(),
|
|
26
|
+
close: createCallTracker(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
// Mock showModal and close for dialog element
|
|
31
|
+
dialogCallState.showModal = createCallTracker();
|
|
32
|
+
dialogCallState.close = createCallTracker();
|
|
33
|
+
HTMLDialogElement.prototype.showModal = function (this: HTMLDialogElement) {
|
|
34
|
+
dialogCallState.showModal.fn();
|
|
35
|
+
this.setAttribute("open", "");
|
|
36
|
+
};
|
|
37
|
+
HTMLDialogElement.prototype.close = function (this: HTMLDialogElement) {
|
|
38
|
+
dialogCallState.close.fn();
|
|
39
|
+
this.removeAttribute("open");
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
HTMLDialogElement.prototype.showModal = originalShowModal;
|
|
45
|
+
HTMLDialogElement.prototype.close = originalClose;
|
|
46
|
+
document.body.style.overflow = "";
|
|
47
|
+
document.body.style.paddingRight = "";
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should render children when visible", () => {
|
|
51
|
+
render(
|
|
52
|
+
<DialogContainer visible={true} onClose={() => {}}>
|
|
53
|
+
<div data-testid="content">Hello</div>
|
|
54
|
+
</DialogContainer>,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(screen.getByTestId("content")).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByTestId("content")).toHaveTextContent("Hello");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should call showModal when visible is true", () => {
|
|
62
|
+
render(
|
|
63
|
+
<DialogContainer visible={true} onClose={() => {}}>
|
|
64
|
+
<div>Content</div>
|
|
65
|
+
</DialogContainer>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(dialogCallState.showModal.calls).toHaveLength(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should call close when visible changes from true to false", () => {
|
|
72
|
+
const { rerender } = render(
|
|
73
|
+
<DialogContainer visible={true} onClose={() => {}}>
|
|
74
|
+
<div>Content</div>
|
|
75
|
+
</DialogContainer>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
rerender(
|
|
79
|
+
<DialogContainer visible={false} onClose={() => {}}>
|
|
80
|
+
<div>Content</div>
|
|
81
|
+
</DialogContainer>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(dialogCallState.close.calls).toHaveLength(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should call onClose when Escape is pressed", () => {
|
|
88
|
+
const onClose = createCallTracker();
|
|
89
|
+
render(
|
|
90
|
+
<DialogContainer visible={true} onClose={onClose.fn}>
|
|
91
|
+
<div>Content</div>
|
|
92
|
+
</DialogContainer>,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const dialog = document.querySelector("dialog");
|
|
96
|
+
expect(dialog).not.toBeNull();
|
|
97
|
+
|
|
98
|
+
// Simulate cancel event (triggered by Escape key)
|
|
99
|
+
fireEvent(dialog!, new Event("cancel", { bubbles: true, cancelable: true }));
|
|
100
|
+
|
|
101
|
+
expect(onClose.calls).toHaveLength(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should NOT call onClose when Escape is pressed and closeOnEscape is false", () => {
|
|
105
|
+
const onClose = createCallTracker();
|
|
106
|
+
render(
|
|
107
|
+
<DialogContainer visible={true} onClose={onClose.fn} closeOnEscape={false}>
|
|
108
|
+
<div>Content</div>
|
|
109
|
+
</DialogContainer>,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const dialog = document.querySelector("dialog");
|
|
113
|
+
fireEvent(dialog!, new Event("cancel", { bubbles: true, cancelable: true }));
|
|
114
|
+
|
|
115
|
+
expect(onClose.calls).toHaveLength(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should call onClose when backdrop is clicked", () => {
|
|
119
|
+
const onClose = createCallTracker();
|
|
120
|
+
render(
|
|
121
|
+
<DialogContainer visible={true} onClose={onClose.fn}>
|
|
122
|
+
<div data-testid="content">Content</div>
|
|
123
|
+
</DialogContainer>,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const dialog = document.querySelector("dialog");
|
|
127
|
+
// Click directly on dialog (backdrop area)
|
|
128
|
+
fireEvent.click(dialog!);
|
|
129
|
+
|
|
130
|
+
expect(onClose.calls).toHaveLength(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should NOT call onClose when content is clicked", () => {
|
|
134
|
+
const onClose = createCallTracker();
|
|
135
|
+
render(
|
|
136
|
+
<DialogContainer visible={true} onClose={onClose.fn}>
|
|
137
|
+
<div data-testid="content">Content</div>
|
|
138
|
+
</DialogContainer>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const content = screen.getByTestId("content");
|
|
142
|
+
fireEvent.click(content);
|
|
143
|
+
|
|
144
|
+
expect(onClose.calls).toHaveLength(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should NOT call onClose when backdrop is clicked and dismissible is false", () => {
|
|
148
|
+
const onClose = createCallTracker();
|
|
149
|
+
render(
|
|
150
|
+
<DialogContainer visible={true} onClose={onClose.fn} dismissible={false}>
|
|
151
|
+
<div>Content</div>
|
|
152
|
+
</DialogContainer>,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const dialog = document.querySelector("dialog");
|
|
156
|
+
fireEvent.click(dialog!);
|
|
157
|
+
|
|
158
|
+
expect(onClose.calls).toHaveLength(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should apply aria attributes", () => {
|
|
162
|
+
render(
|
|
163
|
+
<DialogContainer
|
|
164
|
+
visible={true}
|
|
165
|
+
onClose={() => {}}
|
|
166
|
+
ariaLabel="Test dialog"
|
|
167
|
+
ariaLabelledBy="title-id"
|
|
168
|
+
ariaDescribedBy="desc-id"
|
|
169
|
+
>
|
|
170
|
+
<div>Content</div>
|
|
171
|
+
</DialogContainer>,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const dialog = document.querySelector("dialog");
|
|
175
|
+
expect(dialog).toHaveAttribute("aria-label", "Test dialog");
|
|
176
|
+
expect(dialog).toHaveAttribute("aria-labelledby", "title-id");
|
|
177
|
+
expect(dialog).toHaveAttribute("aria-describedby", "desc-id");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should prevent body scroll when visible", () => {
|
|
181
|
+
render(
|
|
182
|
+
<DialogContainer visible={true} onClose={() => {}}>
|
|
183
|
+
<div>Content</div>
|
|
184
|
+
</DialogContainer>,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(document.body.style.overflow).toBe("hidden");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should restore body scroll on unmount", () => {
|
|
191
|
+
const { unmount } = render(
|
|
192
|
+
<DialogContainer visible={true} onClose={() => {}}>
|
|
193
|
+
<div>Content</div>
|
|
194
|
+
</DialogContainer>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(document.body.style.overflow).toBe("hidden");
|
|
198
|
+
|
|
199
|
+
unmount();
|
|
200
|
+
|
|
201
|
+
expect(document.body.style.overflow).toBe("");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should NOT prevent body scroll when preventBodyScroll is false", () => {
|
|
205
|
+
render(
|
|
206
|
+
<DialogContainer visible={true} onClose={() => {}} preventBodyScroll={false}>
|
|
207
|
+
<div>Content</div>
|
|
208
|
+
</DialogContainer>,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(document.body.style.overflow).toBe("");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should stop propagation on content pointer down to prevent backdrop detection", () => {
|
|
215
|
+
const onClose = createCallTracker();
|
|
216
|
+
render(
|
|
217
|
+
<DialogContainer visible={true} onClose={onClose.fn}>
|
|
218
|
+
<button data-testid="button">Click me</button>
|
|
219
|
+
</DialogContainer>,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const button = screen.getByTestId("button");
|
|
223
|
+
fireEvent.pointerDown(button);
|
|
224
|
+
|
|
225
|
+
// The click should not close the dialog because propagation is stopped
|
|
226
|
+
expect(onClose.calls).toHaveLength(0);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Base dialog container component using native <dialog> element
|
|
3
|
+
*/
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import type { DialogContainerProps } from "./types.js";
|
|
6
|
+
import { useDialogContainer } from "./useDialogContainer.js";
|
|
7
|
+
import { SwipeDialogContainer } from "./SwipeDialogContainer.js";
|
|
8
|
+
import {
|
|
9
|
+
COLOR_MODAL_BACKDROP,
|
|
10
|
+
MODAL_TRANSITION_DURATION,
|
|
11
|
+
MODAL_TRANSITION_EASING,
|
|
12
|
+
} from "../../constants/styles.js";
|
|
13
|
+
|
|
14
|
+
const dialogBaseStyle: React.CSSProperties = {
|
|
15
|
+
border: "none",
|
|
16
|
+
padding: 0,
|
|
17
|
+
background: "transparent",
|
|
18
|
+
maxWidth: "none",
|
|
19
|
+
maxHeight: "none",
|
|
20
|
+
overflow: "visible",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const contentWrapperStyle: React.CSSProperties = {
|
|
24
|
+
display: "flex",
|
|
25
|
+
alignItems: "center",
|
|
26
|
+
justifyContent: "center",
|
|
27
|
+
position: "fixed",
|
|
28
|
+
inset: 0,
|
|
29
|
+
pointerEvents: "none",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const contentStyle: React.CSSProperties = {
|
|
33
|
+
pointerEvents: "auto",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
|
|
37
|
+
|
|
38
|
+
type DialogContainerImplProps = DialogContainerProps;
|
|
39
|
+
|
|
40
|
+
const DialogContainerImpl: React.FC<DialogContainerImplProps> = ({
|
|
41
|
+
visible,
|
|
42
|
+
onClose,
|
|
43
|
+
children,
|
|
44
|
+
position = "center",
|
|
45
|
+
dismissible = true,
|
|
46
|
+
closeOnEscape = true,
|
|
47
|
+
returnFocus = true,
|
|
48
|
+
preventBodyScroll = true,
|
|
49
|
+
ariaLabel,
|
|
50
|
+
ariaLabelledBy,
|
|
51
|
+
ariaDescribedBy,
|
|
52
|
+
transitionMode = "css",
|
|
53
|
+
transitionDuration = MODAL_TRANSITION_DURATION,
|
|
54
|
+
transitionEasing = MODAL_TRANSITION_EASING,
|
|
55
|
+
}) => {
|
|
56
|
+
const { dialogRef, dialogProps } = useDialogContainer({
|
|
57
|
+
visible,
|
|
58
|
+
onClose,
|
|
59
|
+
dismissible,
|
|
60
|
+
closeOnEscape,
|
|
61
|
+
returnFocus,
|
|
62
|
+
preventBodyScroll,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const backdropStyle = React.useMemo((): string => {
|
|
66
|
+
if (transitionMode === "none") {
|
|
67
|
+
return `
|
|
68
|
+
dialog::backdrop {
|
|
69
|
+
background: ${COLOR_MODAL_BACKDROP};
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
return `
|
|
74
|
+
dialog::backdrop {
|
|
75
|
+
background: ${COLOR_MODAL_BACKDROP};
|
|
76
|
+
opacity: 0;
|
|
77
|
+
transition: opacity ${transitionDuration} ${transitionEasing};
|
|
78
|
+
}
|
|
79
|
+
dialog[open]::backdrop {
|
|
80
|
+
opacity: 1;
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
}, [transitionMode, transitionDuration, transitionEasing]);
|
|
84
|
+
|
|
85
|
+
const computedContentWrapperStyle = React.useMemo((): React.CSSProperties => {
|
|
86
|
+
if (position === "center") {
|
|
87
|
+
return contentWrapperStyle;
|
|
88
|
+
}
|
|
89
|
+
// Absolute position
|
|
90
|
+
const style: React.CSSProperties = {
|
|
91
|
+
...contentWrapperStyle,
|
|
92
|
+
alignItems: "flex-start",
|
|
93
|
+
justifyContent: "flex-start",
|
|
94
|
+
};
|
|
95
|
+
if (position.x !== undefined) {
|
|
96
|
+
style.left = position.x;
|
|
97
|
+
}
|
|
98
|
+
if (position.y !== undefined) {
|
|
99
|
+
style.top = position.y;
|
|
100
|
+
}
|
|
101
|
+
return style;
|
|
102
|
+
}, [position]);
|
|
103
|
+
|
|
104
|
+
const computedContentStyle = React.useMemo((): React.CSSProperties => {
|
|
105
|
+
if (transitionMode === "none") {
|
|
106
|
+
return contentStyle;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...contentStyle,
|
|
110
|
+
opacity: visible ? 1 : 0,
|
|
111
|
+
transform: visible ? "scale(1)" : "scale(0.95)",
|
|
112
|
+
transition: `opacity ${transitionDuration} ${transitionEasing}, transform ${transitionDuration} ${transitionEasing}`,
|
|
113
|
+
};
|
|
114
|
+
}, [visible, transitionMode, transitionDuration, transitionEasing]);
|
|
115
|
+
|
|
116
|
+
// Stop propagation on pointer down inside content to prevent backdrop click handling
|
|
117
|
+
const handleContentPointerDown = React.useCallback((event: React.PointerEvent) => {
|
|
118
|
+
event.stopPropagation();
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
<style>{backdropStyle}</style>
|
|
124
|
+
<dialog
|
|
125
|
+
ref={dialogRef}
|
|
126
|
+
style={dialogBaseStyle}
|
|
127
|
+
aria-label={ariaLabel}
|
|
128
|
+
aria-labelledby={ariaLabelledBy}
|
|
129
|
+
aria-describedby={ariaDescribedBy}
|
|
130
|
+
{...dialogProps}
|
|
131
|
+
>
|
|
132
|
+
<div style={computedContentWrapperStyle}>
|
|
133
|
+
<React.Activity mode={visible ? "visible" : "hidden"}>
|
|
134
|
+
<div style={computedContentStyle} onPointerDown={handleContentPointerDown}>
|
|
135
|
+
{children}
|
|
136
|
+
</div>
|
|
137
|
+
</React.Activity>
|
|
138
|
+
</div>
|
|
139
|
+
</dialog>
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Base container for dialog-based overlays using native <dialog> element.
|
|
146
|
+
* Opens in the browser's top layer, ensuring it appears above all other content.
|
|
147
|
+
*
|
|
148
|
+
* Supports three transition modes:
|
|
149
|
+
* - "none": No animation
|
|
150
|
+
* - "css": CSS-based fade/scale animation (default)
|
|
151
|
+
* - "swipe": Swipeable with multi-phase animation
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* <DialogContainer
|
|
156
|
+
* visible={isOpen}
|
|
157
|
+
* onClose={() => setIsOpen(false)}
|
|
158
|
+
* >
|
|
159
|
+
* <div>Dialog content</div>
|
|
160
|
+
* </DialogContainer>
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* @example Swipeable dialog
|
|
164
|
+
* ```tsx
|
|
165
|
+
* <DialogContainer
|
|
166
|
+
* visible={isOpen}
|
|
167
|
+
* onClose={() => setIsOpen(false)}
|
|
168
|
+
* transitionMode="swipe"
|
|
169
|
+
* openDirection="bottom"
|
|
170
|
+
* >
|
|
171
|
+
* <div>Swipe down to close</div>
|
|
172
|
+
* </DialogContainer>
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export const DialogContainer: React.FC<DialogContainerProps> = (props) => {
|
|
176
|
+
if (!isBrowser) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Use SwipeDialogContainer for swipe mode
|
|
181
|
+
if (props.transitionMode === "swipe") {
|
|
182
|
+
return <SwipeDialogContainer {...props} />;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return <DialogContainerImpl {...props} />;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
DialogContainer.displayName = "DialogContainer";
|