react-flow-modal 0.2.0 → 0.4.0
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/README.md +261 -0
- package/dist/index.cjs +13 -11
- package/dist/index.d.cts +9 -5
- package/dist/index.d.ts +9 -5
- package/dist/index.js +12 -11
- package/package.json +1 -1
- package/src/ModalHost.tsx +1 -0
- package/src/index.ts +2 -1
- package/src/useModal.tsx +1 -1
- package/src/useModalContext.ts +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# react-flow-modal
|
|
2
|
+
|
|
3
|
+
Promise-based modal flows for React.
|
|
4
|
+
|
|
5
|
+
`react-flow-modal` lets you treat modals as async flows using
|
|
6
|
+
`Promise` and `async/await`, without coupling your UI to state-driven logic.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add react-flow-modal
|
|
14
|
+
# or
|
|
15
|
+
npm install react-flow-modal
|
|
16
|
+
# or
|
|
17
|
+
yarn add react-flow-modal
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Basic Usage
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { ModalProvider, useModal, renderModals } from "react-flow-modal";
|
|
26
|
+
|
|
27
|
+
function ConfirmModal({
|
|
28
|
+
onConfirm,
|
|
29
|
+
onCancel,
|
|
30
|
+
}: {
|
|
31
|
+
onConfirm: () => void;
|
|
32
|
+
onCancel: () => void;
|
|
33
|
+
}) {
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
style={{
|
|
37
|
+
position: "fixed",
|
|
38
|
+
inset: 0,
|
|
39
|
+
background: "rgba(0,0,0,0.5)",
|
|
40
|
+
display: "grid",
|
|
41
|
+
placeItems: "center",
|
|
42
|
+
zIndex: 1000,
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<div
|
|
46
|
+
style={{
|
|
47
|
+
background: "white",
|
|
48
|
+
padding: 24,
|
|
49
|
+
borderRadius: 8,
|
|
50
|
+
minWidth: 300,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<h3>Are you sure?</h3>
|
|
54
|
+
<p>This action cannot be undone.</p>
|
|
55
|
+
|
|
56
|
+
<div style={{ display: "flex", gap: 8, marginTop: 16 }}>
|
|
57
|
+
<button onClick={onCancel}>Cancel</button>
|
|
58
|
+
<button onClick={onConfirm}>Confirm</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function App() {
|
|
66
|
+
const modal = useModal();
|
|
67
|
+
|
|
68
|
+
const onClick = async () => {
|
|
69
|
+
const result = await modal.open("confirm", (resolve) => (
|
|
70
|
+
<ConfirmModal
|
|
71
|
+
onConfirm={() => resolve(true)}
|
|
72
|
+
onCancel={() => resolve(false)}
|
|
73
|
+
/>
|
|
74
|
+
));
|
|
75
|
+
|
|
76
|
+
console.log("Result:", result);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return <button onClick={onClick}>Open Confirm Modal</button>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ModalRenderer() {
|
|
83
|
+
return renderModals();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default function Root() {
|
|
87
|
+
return (
|
|
88
|
+
<ModalProvider>
|
|
89
|
+
<App />
|
|
90
|
+
<ModalRenderer />
|
|
91
|
+
</ModalProvider>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## With AnimatePresence (Framer Motion)
|
|
99
|
+
|
|
100
|
+
To support exit animations, modals must be rendered inside the same
|
|
101
|
+
React tree as `AnimatePresence`.
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { ModalProvider, useModal, renderModals } from "react-flow-modal";
|
|
105
|
+
import { motion, AnimatePresence } from "motion/react";
|
|
106
|
+
|
|
107
|
+
function ConfirmModal({
|
|
108
|
+
onConfirm,
|
|
109
|
+
onCancel,
|
|
110
|
+
}: {
|
|
111
|
+
onConfirm: () => void;
|
|
112
|
+
onCancel: () => void;
|
|
113
|
+
}) {
|
|
114
|
+
return (
|
|
115
|
+
<motion.div
|
|
116
|
+
initial={{ opacity: 0 }}
|
|
117
|
+
animate={{ opacity: 1 }}
|
|
118
|
+
exit={{ opacity: 0 }}
|
|
119
|
+
style={{
|
|
120
|
+
position: "fixed",
|
|
121
|
+
inset: 0,
|
|
122
|
+
background: "rgba(0,0,0,0.5)",
|
|
123
|
+
display: "grid",
|
|
124
|
+
placeItems: "center",
|
|
125
|
+
zIndex: 1000,
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
<motion.div
|
|
129
|
+
initial={{ scale: 0.9, opacity: 0 }}
|
|
130
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
131
|
+
exit={{ scale: 0.95, opacity: 0 }}
|
|
132
|
+
transition={{ duration: 0.2 }}
|
|
133
|
+
style={{
|
|
134
|
+
background: "white",
|
|
135
|
+
padding: 24,
|
|
136
|
+
borderRadius: 8,
|
|
137
|
+
minWidth: 300,
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<h3>Are you sure?</h3>
|
|
141
|
+
<p>This action cannot be undone.</p>
|
|
142
|
+
|
|
143
|
+
<div style={{ display: "flex", gap: 8, marginTop: 16 }}>
|
|
144
|
+
<button onClick={onCancel}>Cancel</button>
|
|
145
|
+
<button onClick={onConfirm}>Confirm</button>
|
|
146
|
+
</div>
|
|
147
|
+
</motion.div>
|
|
148
|
+
</motion.div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function App() {
|
|
153
|
+
const modal = useModal();
|
|
154
|
+
|
|
155
|
+
const onClick = async () => {
|
|
156
|
+
const result = await modal.open("confirm", (resolve) => (
|
|
157
|
+
<ConfirmModal
|
|
158
|
+
onConfirm={() => resolve(true)}
|
|
159
|
+
onCancel={() => resolve(false)}
|
|
160
|
+
/>
|
|
161
|
+
));
|
|
162
|
+
|
|
163
|
+
console.log("Result:", result);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return <button onClick={onClick}>Open Confirm Modal</button>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function ModalRenderer() {
|
|
170
|
+
return (
|
|
171
|
+
<AnimatePresence>
|
|
172
|
+
{renderModals()}
|
|
173
|
+
</AnimatePresence>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export default function Root() {
|
|
178
|
+
return (
|
|
179
|
+
<ModalProvider>
|
|
180
|
+
<App />
|
|
181
|
+
<ModalRenderer />
|
|
182
|
+
</ModalProvider>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## API
|
|
190
|
+
|
|
191
|
+
### useModal
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const modal = useModal();
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Returns an object that controls the modal flow.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
{
|
|
201
|
+
open<T>(
|
|
202
|
+
key: string,
|
|
203
|
+
render: (
|
|
204
|
+
resolve: (value: T) => void,
|
|
205
|
+
reject: (reason?: unknown) => void
|
|
206
|
+
) => React.ReactNode
|
|
207
|
+
): Promise<T>;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### renderModals
|
|
212
|
+
```ts
|
|
213
|
+
renderModals(): React.ReactNode
|
|
214
|
+
```
|
|
215
|
+
Renders the entire modal stack. This function should be rendered **once** in your React tree.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Important
|
|
220
|
+
|
|
221
|
+
> ⚠️ Always resolve or reject the promise.
|
|
222
|
+
> Leaving it pending will block the async flow.
|
|
223
|
+
|
|
224
|
+
> ⚠️ `renderModals()` should be rendered once.
|
|
225
|
+
> Rendering it multiple times may result in duplicated modals.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Why react-flow-modal?
|
|
230
|
+
|
|
231
|
+
Most modal libraries are state-driven:
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
setOpen(true);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
This makes modal control implicit and tightly coupled to rendering.
|
|
238
|
+
|
|
239
|
+
`react-flow-modal` treats modals as explicit async control points:
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
const result = await open(...);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
This keeps control flow readable, composable, and testable.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Features
|
|
250
|
+
|
|
251
|
+
* Headless API (no styles, no UI constraints)
|
|
252
|
+
* Promise-based modal control
|
|
253
|
+
* Internal stack management
|
|
254
|
+
* Render location fully controlled by the user
|
|
255
|
+
* Works naturally with async / await
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ModalHost: () => ModalHost,
|
|
24
24
|
ModalProvider: () => ModalProvider,
|
|
25
|
+
renderModals: () => renderModals,
|
|
25
26
|
useModal: () => useModal
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -43,9 +44,6 @@ var ModalProvider = ({ children }) => {
|
|
|
43
44
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalContext.Provider, { value: { stack, setStack }, children });
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
// src/ModalHost.tsx
|
|
47
|
-
var import_react4 = require("react");
|
|
48
|
-
|
|
49
47
|
// src/useModalContext.ts
|
|
50
48
|
var import_react3 = require("react");
|
|
51
49
|
var useModalContext = () => {
|
|
@@ -55,17 +53,12 @@ var useModalContext = () => {
|
|
|
55
53
|
}
|
|
56
54
|
return context;
|
|
57
55
|
};
|
|
58
|
-
|
|
59
|
-
// src/ModalHost.tsx
|
|
60
|
-
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
61
|
-
var ModalHost = () => {
|
|
56
|
+
var renderModals = () => {
|
|
62
57
|
const { stack } = useModalContext();
|
|
63
|
-
return
|
|
58
|
+
return stack.map((item) => item);
|
|
64
59
|
};
|
|
65
60
|
|
|
66
61
|
// src/useModal.tsx
|
|
67
|
-
var import_react5 = require("react");
|
|
68
|
-
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
69
62
|
var useModal = () => {
|
|
70
63
|
const { setStack } = useModalContext();
|
|
71
64
|
const pop = () => {
|
|
@@ -83,14 +76,23 @@ var useModal = () => {
|
|
|
83
76
|
reject(reason);
|
|
84
77
|
pop();
|
|
85
78
|
});
|
|
86
|
-
push(
|
|
79
|
+
push(element);
|
|
87
80
|
});
|
|
88
81
|
};
|
|
89
82
|
return { open };
|
|
90
83
|
};
|
|
84
|
+
|
|
85
|
+
// src/ModalHost.tsx
|
|
86
|
+
var import_react4 = require("react");
|
|
87
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
88
|
+
var ModalHost = () => {
|
|
89
|
+
const { stack } = useModalContext();
|
|
90
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Fragment, { children: stack.map((item) => item) });
|
|
91
|
+
};
|
|
91
92
|
// Annotate the CommonJS export names for ESM import in node:
|
|
92
93
|
0 && (module.exports = {
|
|
93
94
|
ModalHost,
|
|
94
95
|
ModalProvider,
|
|
96
|
+
renderModals,
|
|
95
97
|
useModal
|
|
96
98
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import react__default, { FC, PropsWithChildren } from 'react';
|
|
2
3
|
|
|
3
4
|
declare const ModalProvider: FC<PropsWithChildren>;
|
|
4
5
|
|
|
5
|
-
declare const ModalHost: FC;
|
|
6
|
-
|
|
7
6
|
declare const useModal: () => {
|
|
8
|
-
open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) =>
|
|
7
|
+
open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) => react__default.ReactNode) => Promise<T>;
|
|
9
8
|
};
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
/** @deprecated use renderModals instead */
|
|
11
|
+
declare const ModalHost: FC;
|
|
12
|
+
|
|
13
|
+
declare const renderModals: () => react.ReactNode[];
|
|
14
|
+
|
|
15
|
+
export { ModalHost, ModalProvider, renderModals, useModal };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import react__default, { FC, PropsWithChildren } from 'react';
|
|
2
3
|
|
|
3
4
|
declare const ModalProvider: FC<PropsWithChildren>;
|
|
4
5
|
|
|
5
|
-
declare const ModalHost: FC;
|
|
6
|
-
|
|
7
6
|
declare const useModal: () => {
|
|
8
|
-
open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) =>
|
|
7
|
+
open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) => react__default.ReactNode) => Promise<T>;
|
|
9
8
|
};
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
/** @deprecated use renderModals instead */
|
|
11
|
+
declare const ModalHost: FC;
|
|
12
|
+
|
|
13
|
+
declare const renderModals: () => react.ReactNode[];
|
|
14
|
+
|
|
15
|
+
export { ModalHost, ModalProvider, renderModals, useModal };
|
package/dist/index.js
CHANGED
|
@@ -15,9 +15,6 @@ var ModalProvider = ({ children }) => {
|
|
|
15
15
|
return /* @__PURE__ */ jsx(ModalContext.Provider, { value: { stack, setStack }, children });
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
// src/ModalHost.tsx
|
|
19
|
-
import { Fragment } from "react";
|
|
20
|
-
|
|
21
18
|
// src/useModalContext.ts
|
|
22
19
|
import { useContext } from "react";
|
|
23
20
|
var useModalContext = () => {
|
|
@@ -27,17 +24,12 @@ var useModalContext = () => {
|
|
|
27
24
|
}
|
|
28
25
|
return context;
|
|
29
26
|
};
|
|
30
|
-
|
|
31
|
-
// src/ModalHost.tsx
|
|
32
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
33
|
-
var ModalHost = () => {
|
|
27
|
+
var renderModals = () => {
|
|
34
28
|
const { stack } = useModalContext();
|
|
35
|
-
return
|
|
29
|
+
return stack.map((item) => item);
|
|
36
30
|
};
|
|
37
31
|
|
|
38
32
|
// src/useModal.tsx
|
|
39
|
-
import { Fragment as Fragment2 } from "react";
|
|
40
|
-
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
41
33
|
var useModal = () => {
|
|
42
34
|
const { setStack } = useModalContext();
|
|
43
35
|
const pop = () => {
|
|
@@ -55,13 +47,22 @@ var useModal = () => {
|
|
|
55
47
|
reject(reason);
|
|
56
48
|
pop();
|
|
57
49
|
});
|
|
58
|
-
push(
|
|
50
|
+
push(element);
|
|
59
51
|
});
|
|
60
52
|
};
|
|
61
53
|
return { open };
|
|
62
54
|
};
|
|
55
|
+
|
|
56
|
+
// src/ModalHost.tsx
|
|
57
|
+
import { Fragment } from "react";
|
|
58
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
59
|
+
var ModalHost = () => {
|
|
60
|
+
const { stack } = useModalContext();
|
|
61
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: stack.map((item) => item) });
|
|
62
|
+
};
|
|
63
63
|
export {
|
|
64
64
|
ModalHost,
|
|
65
65
|
ModalProvider,
|
|
66
|
+
renderModals,
|
|
66
67
|
useModal
|
|
67
68
|
};
|
package/package.json
CHANGED
package/src/ModalHost.tsx
CHANGED
package/src/index.ts
CHANGED
package/src/useModal.tsx
CHANGED