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,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tests for Modal component
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
5
|
+
import { Modal } from "./Modal";
|
|
6
|
+
|
|
7
|
+
type CallTracker = {
|
|
8
|
+
calls: ReadonlyArray<ReadonlyArray<unknown>>;
|
|
9
|
+
fn: (...args: ReadonlyArray<unknown>) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const createCallTracker = (): CallTracker => {
|
|
13
|
+
const calls: Array<ReadonlyArray<unknown>> = [];
|
|
14
|
+
const fn = (...args: ReadonlyArray<unknown>): void => {
|
|
15
|
+
calls.push(args);
|
|
16
|
+
};
|
|
17
|
+
return { calls, fn };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("Modal", () => {
|
|
21
|
+
const originalShowModal = HTMLDialogElement.prototype.showModal;
|
|
22
|
+
const originalClose = HTMLDialogElement.prototype.close;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
HTMLDialogElement.prototype.showModal = function (this: HTMLDialogElement) {
|
|
26
|
+
this.setAttribute("open", "");
|
|
27
|
+
};
|
|
28
|
+
HTMLDialogElement.prototype.close = function (this: HTMLDialogElement) {
|
|
29
|
+
this.removeAttribute("open");
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
HTMLDialogElement.prototype.showModal = originalShowModal;
|
|
35
|
+
HTMLDialogElement.prototype.close = originalClose;
|
|
36
|
+
document.body.style.overflow = "";
|
|
37
|
+
document.body.style.paddingRight = "";
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should render children when visible", () => {
|
|
41
|
+
render(
|
|
42
|
+
<Modal visible={true} onClose={() => {}}>
|
|
43
|
+
<div data-testid="content">Modal content</div>
|
|
44
|
+
</Modal>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByTestId("content")).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByTestId("content")).toHaveTextContent("Modal content");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should render header with title when provided", () => {
|
|
52
|
+
render(
|
|
53
|
+
<Modal visible={true} onClose={() => {}} header={{ title: "Test Title" }}>
|
|
54
|
+
<div>Content</div>
|
|
55
|
+
</Modal>,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(screen.getByText("Test Title")).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should render close button in header when showCloseButton is true", () => {
|
|
62
|
+
render(
|
|
63
|
+
<Modal visible={true} onClose={() => {}} header={{ title: "Title", showCloseButton: true }}>
|
|
64
|
+
<div>Content</div>
|
|
65
|
+
</Modal>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(screen.getByRole("button", { name: "Close modal" })).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should NOT render close button when showCloseButton is false", () => {
|
|
72
|
+
render(
|
|
73
|
+
<Modal visible={true} onClose={() => {}} header={{ title: "Title", showCloseButton: false }}>
|
|
74
|
+
<div>Content</div>
|
|
75
|
+
</Modal>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(screen.queryByRole("button", { name: "Close modal" })).not.toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should NOT render close button when dismissible is false", () => {
|
|
82
|
+
render(
|
|
83
|
+
<Modal visible={true} onClose={() => {}} dismissible={false} header={{ title: "Title", showCloseButton: true }}>
|
|
84
|
+
<div>Content</div>
|
|
85
|
+
</Modal>,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(screen.queryByRole("button", { name: "Close modal" })).not.toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should call onClose when close button is clicked", () => {
|
|
92
|
+
const onClose = createCallTracker();
|
|
93
|
+
render(
|
|
94
|
+
<Modal visible={true} onClose={onClose.fn} header={{ title: "Title" }}>
|
|
95
|
+
<div>Content</div>
|
|
96
|
+
</Modal>,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const closeButton = screen.getByRole("button", { name: "Close modal" });
|
|
100
|
+
fireEvent.click(closeButton);
|
|
101
|
+
|
|
102
|
+
expect(onClose.calls).toHaveLength(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should call onClose when Escape is pressed", () => {
|
|
106
|
+
const onClose = createCallTracker();
|
|
107
|
+
render(
|
|
108
|
+
<Modal visible={true} onClose={onClose.fn}>
|
|
109
|
+
<div>Content</div>
|
|
110
|
+
</Modal>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const dialog = document.querySelector("dialog");
|
|
114
|
+
fireEvent(dialog!, new Event("cancel", { bubbles: true, cancelable: true }));
|
|
115
|
+
|
|
116
|
+
expect(onClose.calls).toHaveLength(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should call onClose when backdrop is clicked", () => {
|
|
120
|
+
const onClose = createCallTracker();
|
|
121
|
+
render(
|
|
122
|
+
<Modal visible={true} onClose={onClose.fn}>
|
|
123
|
+
<div data-testid="content">Content</div>
|
|
124
|
+
</Modal>,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const dialog = document.querySelector("dialog");
|
|
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
|
+
<Modal visible={true} onClose={onClose.fn}>
|
|
137
|
+
<div data-testid="content">Content</div>
|
|
138
|
+
</Modal>,
|
|
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 render chrome when chrome is false", () => {
|
|
148
|
+
render(
|
|
149
|
+
<Modal visible={true} onClose={() => {}} chrome={false} header={{ title: "Title" }}>
|
|
150
|
+
<div data-testid="content">Content</div>
|
|
151
|
+
</Modal>,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Header should not be rendered when chrome is false
|
|
155
|
+
expect(screen.queryByText("Title")).not.toBeInTheDocument();
|
|
156
|
+
expect(screen.getByTestId("content")).toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should set aria-label from header title", () => {
|
|
160
|
+
render(
|
|
161
|
+
<Modal visible={true} onClose={() => {}} header={{ title: "Settings Dialog" }}>
|
|
162
|
+
<div>Content</div>
|
|
163
|
+
</Modal>,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const dialog = document.querySelector("dialog");
|
|
167
|
+
expect(dialog).toHaveAttribute("aria-label", "Settings Dialog");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should use custom aria-label when provided", () => {
|
|
171
|
+
render(
|
|
172
|
+
<Modal visible={true} onClose={() => {}} ariaLabel="Custom Label" header={{ title: "Title" }}>
|
|
173
|
+
<div>Content</div>
|
|
174
|
+
</Modal>,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const dialog = document.querySelector("dialog");
|
|
178
|
+
expect(dialog).toHaveAttribute("aria-label", "Custom Label");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should handle input focus without conflict", () => {
|
|
182
|
+
render(
|
|
183
|
+
<Modal visible={true} onClose={() => {}}>
|
|
184
|
+
<input data-testid="input" type="text" />
|
|
185
|
+
</Modal>,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const input = screen.getByTestId("input") as HTMLInputElement;
|
|
189
|
+
input.focus();
|
|
190
|
+
fireEvent.change(input, { target: { value: "test" } });
|
|
191
|
+
|
|
192
|
+
expect(input.value).toBe("test");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should render with custom width and height", () => {
|
|
196
|
+
render(
|
|
197
|
+
<Modal visible={true} onClose={() => {}} width={500} height={400}>
|
|
198
|
+
<div data-testid="content">Content</div>
|
|
199
|
+
</Modal>,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const content = screen.getByTestId("content");
|
|
203
|
+
// Traverse: FloatingPanelContent > inner > overflow > shadow > computedStyle div
|
|
204
|
+
const styledDiv = content.parentElement?.parentElement?.parentElement?.parentElement?.parentElement;
|
|
205
|
+
expect(styledDiv).toHaveStyle({ width: "500px", height: "400px" });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should render with string width and height", () => {
|
|
209
|
+
render(
|
|
210
|
+
<Modal visible={true} onClose={() => {}} width="80%" height="auto">
|
|
211
|
+
<div data-testid="content">Content</div>
|
|
212
|
+
</Modal>,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const content = screen.getByTestId("content");
|
|
216
|
+
// Traverse: FloatingPanelContent > inner > overflow > shadow > computedStyle div
|
|
217
|
+
const styledDiv = content.parentElement?.parentElement?.parentElement?.parentElement?.parentElement;
|
|
218
|
+
expect(styledDiv).toHaveStyle({ width: "80%", height: "auto" });
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Modal component - Centered modal dialog with optional chrome
|
|
3
|
+
*/
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import type { ModalProps } from "./types";
|
|
6
|
+
import { DialogContainer } from "./DialogContainer";
|
|
7
|
+
import {
|
|
8
|
+
FloatingPanelFrame,
|
|
9
|
+
FloatingPanelHeader,
|
|
10
|
+
FloatingPanelTitle,
|
|
11
|
+
FloatingPanelContent,
|
|
12
|
+
FloatingPanelCloseButton,
|
|
13
|
+
} from "../../components/paneling/FloatingPanelFrame";
|
|
14
|
+
import {
|
|
15
|
+
MODAL_MIN_WIDTH,
|
|
16
|
+
MODAL_MAX_WIDTH,
|
|
17
|
+
MODAL_MAX_HEIGHT,
|
|
18
|
+
FLOATING_PANEL_HEADER_PADDING_Y,
|
|
19
|
+
FLOATING_PANEL_HEADER_PADDING_X,
|
|
20
|
+
FLOATING_PANEL_GAP,
|
|
21
|
+
} from "../../constants/styles";
|
|
22
|
+
|
|
23
|
+
const modalContentStyle: React.CSSProperties = {
|
|
24
|
+
minWidth: MODAL_MIN_WIDTH,
|
|
25
|
+
maxWidth: MODAL_MAX_WIDTH,
|
|
26
|
+
maxHeight: MODAL_MAX_HEIGHT,
|
|
27
|
+
display: "flex",
|
|
28
|
+
flexDirection: "column",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type ModalContentProps = {
|
|
32
|
+
header?: ModalProps["header"];
|
|
33
|
+
dismissible: boolean;
|
|
34
|
+
onClose: () => void;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
contentStyle?: React.CSSProperties;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const ModalContentWithChrome: React.FC<ModalContentProps> = ({
|
|
40
|
+
header,
|
|
41
|
+
dismissible,
|
|
42
|
+
onClose,
|
|
43
|
+
children,
|
|
44
|
+
contentStyle,
|
|
45
|
+
}) => {
|
|
46
|
+
const showCloseButton = header?.showCloseButton ?? true;
|
|
47
|
+
const shouldShowClose = dismissible ? showCloseButton : false;
|
|
48
|
+
|
|
49
|
+
const hasHeader = header !== undefined;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<FloatingPanelFrame>
|
|
53
|
+
<React.Activity mode={hasHeader ? "visible" : "hidden"}>
|
|
54
|
+
<FloatingPanelHeader
|
|
55
|
+
style={{
|
|
56
|
+
padding: `${FLOATING_PANEL_HEADER_PADDING_Y} ${FLOATING_PANEL_HEADER_PADDING_X}`,
|
|
57
|
+
gap: FLOATING_PANEL_GAP,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<FloatingPanelTitle>{header?.title}</FloatingPanelTitle>
|
|
61
|
+
<React.Activity mode={shouldShowClose ? "visible" : "hidden"}>
|
|
62
|
+
<FloatingPanelCloseButton onClick={onClose} aria-label="Close modal" style={{ marginLeft: "auto" }} />
|
|
63
|
+
</React.Activity>
|
|
64
|
+
</FloatingPanelHeader>
|
|
65
|
+
</React.Activity>
|
|
66
|
+
<FloatingPanelContent style={{ flex: 1, overflow: "auto", ...contentStyle }}>{children}</FloatingPanelContent>
|
|
67
|
+
</FloatingPanelFrame>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type ModalContentRendererProps = ModalContentProps & {
|
|
72
|
+
chrome: boolean;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const ModalContentRenderer: React.FC<ModalContentRendererProps> = ({ chrome, children, ...chromeProps }) => {
|
|
76
|
+
if (chrome) {
|
|
77
|
+
return <ModalContentWithChrome {...chromeProps}>{children}</ModalContentWithChrome>;
|
|
78
|
+
}
|
|
79
|
+
return <>{children}</>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Modal component for displaying centered dialogs.
|
|
84
|
+
* Uses native <dialog> element for proper accessibility and top-layer rendering.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* const [isOpen, setIsOpen] = useState(false);
|
|
89
|
+
*
|
|
90
|
+
* <Modal
|
|
91
|
+
* visible={isOpen}
|
|
92
|
+
* onClose={() => setIsOpen(false)}
|
|
93
|
+
* header={{ title: "Settings" }}
|
|
94
|
+
* >
|
|
95
|
+
* <form>
|
|
96
|
+
* <input type="text" placeholder="Name" />
|
|
97
|
+
* <button type="submit">Save</button>
|
|
98
|
+
* </form>
|
|
99
|
+
* </Modal>
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export const Modal: React.FC<ModalProps> = ({
|
|
103
|
+
visible,
|
|
104
|
+
onClose,
|
|
105
|
+
children,
|
|
106
|
+
header,
|
|
107
|
+
width,
|
|
108
|
+
height,
|
|
109
|
+
maxWidth,
|
|
110
|
+
maxHeight,
|
|
111
|
+
chrome = true,
|
|
112
|
+
dismissible = true,
|
|
113
|
+
closeOnEscape = true,
|
|
114
|
+
preventBodyScroll = true,
|
|
115
|
+
returnFocus = true,
|
|
116
|
+
ariaLabel,
|
|
117
|
+
ariaLabelledBy,
|
|
118
|
+
ariaDescribedBy,
|
|
119
|
+
transitionMode = "css",
|
|
120
|
+
transitionDuration,
|
|
121
|
+
transitionEasing,
|
|
122
|
+
contentStyle: propContentStyle,
|
|
123
|
+
swipeDismissible,
|
|
124
|
+
openDirection = "center",
|
|
125
|
+
useViewTransition = false,
|
|
126
|
+
}) => {
|
|
127
|
+
const computedStyle = React.useMemo((): React.CSSProperties => {
|
|
128
|
+
const style: React.CSSProperties = { ...modalContentStyle };
|
|
129
|
+
|
|
130
|
+
if (width !== undefined) {
|
|
131
|
+
style.width = typeof width === "number" ? `${width}px` : width;
|
|
132
|
+
}
|
|
133
|
+
if (height !== undefined) {
|
|
134
|
+
style.height = typeof height === "number" ? `${height}px` : height;
|
|
135
|
+
}
|
|
136
|
+
if (maxWidth !== undefined) {
|
|
137
|
+
style.maxWidth = typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth;
|
|
138
|
+
}
|
|
139
|
+
if (maxHeight !== undefined) {
|
|
140
|
+
style.maxHeight = typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return style;
|
|
144
|
+
}, [width, height, maxWidth, maxHeight]);
|
|
145
|
+
|
|
146
|
+
const effectiveAriaLabel = ariaLabel ?? header?.title ?? "Modal";
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<DialogContainer
|
|
150
|
+
visible={visible}
|
|
151
|
+
onClose={onClose}
|
|
152
|
+
position="center"
|
|
153
|
+
dismissible={dismissible}
|
|
154
|
+
closeOnEscape={closeOnEscape}
|
|
155
|
+
preventBodyScroll={preventBodyScroll}
|
|
156
|
+
returnFocus={returnFocus}
|
|
157
|
+
ariaLabel={effectiveAriaLabel}
|
|
158
|
+
ariaLabelledBy={ariaLabelledBy}
|
|
159
|
+
ariaDescribedBy={ariaDescribedBy}
|
|
160
|
+
transitionMode={transitionMode}
|
|
161
|
+
transitionDuration={transitionDuration}
|
|
162
|
+
transitionEasing={transitionEasing}
|
|
163
|
+
swipeDismissible={swipeDismissible}
|
|
164
|
+
openDirection={openDirection}
|
|
165
|
+
useViewTransition={useViewTransition}
|
|
166
|
+
>
|
|
167
|
+
<div style={computedStyle}>
|
|
168
|
+
<ModalContentRenderer
|
|
169
|
+
chrome={chrome}
|
|
170
|
+
header={header}
|
|
171
|
+
dismissible={dismissible}
|
|
172
|
+
onClose={onClose}
|
|
173
|
+
contentStyle={propContentStyle}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
</ModalContentRenderer>
|
|
177
|
+
</div>
|
|
178
|
+
</DialogContainer>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
Modal.displayName = "Modal";
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Swipeable dialog container component.
|
|
3
|
+
*
|
|
4
|
+
* This component extends the base dialog functionality with swipe-to-dismiss
|
|
5
|
+
* and multi-phase animations. It uses native <dialog> element for top layer
|
|
6
|
+
* positioning and custom transform animations.
|
|
7
|
+
*/
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import type { DialogContainerProps } from "./types.js";
|
|
10
|
+
import { useDialogContainer } from "./useDialogContainer.js";
|
|
11
|
+
import { useDialogSwipeInput } from "./useDialogSwipeInput.js";
|
|
12
|
+
import { useDialogTransform } from "./useDialogTransform.js";
|
|
13
|
+
import { COLOR_MODAL_BACKDROP } from "../../constants/styles.js";
|
|
14
|
+
|
|
15
|
+
const dialogBaseStyle: React.CSSProperties = {
|
|
16
|
+
border: "none",
|
|
17
|
+
padding: 0,
|
|
18
|
+
background: "transparent",
|
|
19
|
+
maxWidth: "none",
|
|
20
|
+
maxHeight: "none",
|
|
21
|
+
overflow: "visible",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const contentWrapperStyle: React.CSSProperties = {
|
|
25
|
+
display: "flex",
|
|
26
|
+
alignItems: "center",
|
|
27
|
+
justifyContent: "center",
|
|
28
|
+
position: "fixed",
|
|
29
|
+
inset: 0,
|
|
30
|
+
pointerEvents: "none",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const contentStyle: React.CSSProperties = {
|
|
34
|
+
pointerEvents: "auto",
|
|
35
|
+
willChange: "transform, opacity",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const backdropStyle: React.CSSProperties = {
|
|
39
|
+
position: "fixed",
|
|
40
|
+
inset: 0,
|
|
41
|
+
background: COLOR_MODAL_BACKDROP,
|
|
42
|
+
pointerEvents: "auto",
|
|
43
|
+
willChange: "opacity",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Swipeable dialog container with multi-phase animation.
|
|
48
|
+
*
|
|
49
|
+
* Uses native <dialog> for top layer positioning and custom JS animations
|
|
50
|
+
* for swipe-to-dismiss functionality.
|
|
51
|
+
*/
|
|
52
|
+
export const SwipeDialogContainer: React.FC<DialogContainerProps> = ({
|
|
53
|
+
visible,
|
|
54
|
+
onClose,
|
|
55
|
+
children,
|
|
56
|
+
position = "center",
|
|
57
|
+
dismissible = true,
|
|
58
|
+
closeOnEscape = true,
|
|
59
|
+
returnFocus = true,
|
|
60
|
+
preventBodyScroll = true,
|
|
61
|
+
ariaLabel,
|
|
62
|
+
ariaLabelledBy,
|
|
63
|
+
ariaDescribedBy,
|
|
64
|
+
swipeDismissible = true,
|
|
65
|
+
openDirection = "center",
|
|
66
|
+
useViewTransition = false,
|
|
67
|
+
}) => {
|
|
68
|
+
const contentRef = React.useRef<HTMLDivElement>(null);
|
|
69
|
+
const backdropRef = React.useRef<HTMLDivElement>(null);
|
|
70
|
+
|
|
71
|
+
// Base dialog lifecycle management
|
|
72
|
+
const { dialogRef, dialogProps } = useDialogContainer({
|
|
73
|
+
visible,
|
|
74
|
+
onClose,
|
|
75
|
+
dismissible,
|
|
76
|
+
closeOnEscape,
|
|
77
|
+
returnFocus,
|
|
78
|
+
preventBodyScroll,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Swipe input detection - free 2D movement
|
|
82
|
+
const swipeEnabled = swipeDismissible ? visible : false;
|
|
83
|
+
const {
|
|
84
|
+
state: swipeState,
|
|
85
|
+
containerProps: swipeContainerProps,
|
|
86
|
+
displacement,
|
|
87
|
+
displacement2D,
|
|
88
|
+
isOperating,
|
|
89
|
+
} = useDialogSwipeInput({
|
|
90
|
+
containerRef: contentRef,
|
|
91
|
+
openDirection,
|
|
92
|
+
enabled: swipeEnabled,
|
|
93
|
+
onSwipeDismiss: onClose,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Transform and animation management
|
|
97
|
+
const { phase, isAnimating, triggerClose } = useDialogTransform({
|
|
98
|
+
elementRef: contentRef,
|
|
99
|
+
backdropRef,
|
|
100
|
+
visible,
|
|
101
|
+
openDirection,
|
|
102
|
+
swipeState,
|
|
103
|
+
displacement,
|
|
104
|
+
displacement2D,
|
|
105
|
+
useViewTransition,
|
|
106
|
+
onCloseComplete: onClose,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Handle backdrop click
|
|
110
|
+
const handleBackdropClick = React.useCallback(() => {
|
|
111
|
+
if (!dismissible) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (isOperating) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (isAnimating) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
triggerClose();
|
|
121
|
+
}, [dismissible, isOperating, isAnimating, triggerClose]);
|
|
122
|
+
|
|
123
|
+
// Compute content wrapper style based on position
|
|
124
|
+
const computedContentWrapperStyle = React.useMemo((): React.CSSProperties => {
|
|
125
|
+
if (position === "center") {
|
|
126
|
+
return contentWrapperStyle;
|
|
127
|
+
}
|
|
128
|
+
// Absolute position
|
|
129
|
+
const style: React.CSSProperties = {
|
|
130
|
+
...contentWrapperStyle,
|
|
131
|
+
alignItems: "flex-start",
|
|
132
|
+
justifyContent: "flex-start",
|
|
133
|
+
};
|
|
134
|
+
if (position.x !== undefined) {
|
|
135
|
+
style.left = position.x;
|
|
136
|
+
}
|
|
137
|
+
if (position.y !== undefined) {
|
|
138
|
+
style.top = position.y;
|
|
139
|
+
}
|
|
140
|
+
return style;
|
|
141
|
+
}, [position]);
|
|
142
|
+
|
|
143
|
+
// Merge swipe container props with content style
|
|
144
|
+
// Note: We don't use React.Activity here because it uses display:none,
|
|
145
|
+
// which prevents dimension measurement needed for animations.
|
|
146
|
+
// Instead, we use visibility and opacity controlled by the animation.
|
|
147
|
+
const mergedContentStyle = React.useMemo((): React.CSSProperties => {
|
|
148
|
+
const style: React.CSSProperties = {
|
|
149
|
+
...contentStyle,
|
|
150
|
+
...swipeContainerProps.style,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// When closed, hide the content but keep it in layout for dimension measurement
|
|
154
|
+
if (phase === "closed") {
|
|
155
|
+
style.visibility = "hidden";
|
|
156
|
+
style.pointerEvents = "none";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return style;
|
|
160
|
+
}, [swipeContainerProps.style, phase]);
|
|
161
|
+
|
|
162
|
+
// Stop propagation on content pointer events
|
|
163
|
+
const handleContentPointerDown = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
|
164
|
+
swipeContainerProps.onPointerDown?.(event);
|
|
165
|
+
// Don't stop propagation - let swipe input handle it
|
|
166
|
+
}, [swipeContainerProps]);
|
|
167
|
+
|
|
168
|
+
// Hide dialog styling (we use our own backdrop)
|
|
169
|
+
const hideNativeBackdrop = `
|
|
170
|
+
dialog::backdrop {
|
|
171
|
+
background: transparent;
|
|
172
|
+
}
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
<style>{hideNativeBackdrop}</style>
|
|
178
|
+
<dialog
|
|
179
|
+
ref={dialogRef}
|
|
180
|
+
style={dialogBaseStyle}
|
|
181
|
+
aria-label={ariaLabel}
|
|
182
|
+
aria-labelledby={ariaLabelledBy}
|
|
183
|
+
aria-describedby={ariaDescribedBy}
|
|
184
|
+
{...dialogProps}
|
|
185
|
+
>
|
|
186
|
+
{/* Custom backdrop for animated opacity */}
|
|
187
|
+
<div
|
|
188
|
+
ref={backdropRef}
|
|
189
|
+
style={backdropStyle}
|
|
190
|
+
onClick={handleBackdropClick}
|
|
191
|
+
aria-hidden="true"
|
|
192
|
+
/>
|
|
193
|
+
|
|
194
|
+
<div style={computedContentWrapperStyle}>
|
|
195
|
+
<div
|
|
196
|
+
ref={contentRef}
|
|
197
|
+
style={mergedContentStyle}
|
|
198
|
+
onPointerDown={handleContentPointerDown}
|
|
199
|
+
>
|
|
200
|
+
{children}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</dialog>
|
|
204
|
+
</>
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
SwipeDialogContainer.displayName = "SwipeDialogContainer";
|