react-modal-port 0.2.6 → 1.0.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/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +74 -0
- package/dist/index.cjs +220 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +194 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -45
- package/readme.md +245 -234
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/dist/index.modern.mjs +0 -2
- package/dist/index.modern.mjs.map +0 -1
- package/dist/index.umd.js +0 -2
- package/dist/index.umd.js.map +0 -1
- package/types/context.d.ts +0 -20
- package/types/context.d.ts.map +0 -1
- package/types/index.d.ts +0 -3
- package/types/index.d.ts.map +0 -1
- package/types/modal-port.d.ts +0 -4
- package/types/modal-port.d.ts.map +0 -1
- package/types/setupTests.d.ts +0 -1
- package/types/types.d.ts +0 -34
package/readme.md
CHANGED
|
@@ -1,313 +1,324 @@
|
|
|
1
1
|
# react-modal-port
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* Define and style modals the way *you* want or need.
|
|
5
|
-
* Launch modals flexibly from anywhere in your React app.
|
|
6
|
-
* Have them render at one specific place in your DOM.
|
|
3
|
+
Launch modals from any React component, render them in one place, and have TypeScript check every launch.
|
|
7
4
|
|
|
8
|
-
**
|
|
5
|
+
- **Bring your own UI.** Modals and backdrops are your components; the library ships no styles or markup.
|
|
6
|
+
- **Launch from anywhere.** `useModal()` returns a stable `launchModal` function that can be called from components, effects or other modals.
|
|
7
|
+
- **One outlet.** `<ModalPort>` renders the top modal of a stack, wherever you put it (or into a portal).
|
|
8
|
+
- **Type-safe.** Resolvers and props are checked against the modal component's props.
|
|
9
|
+
- **Async-aware stacking.** A modal closes when one of its resolvers settles, even if other modals were stacked on top in the meantime.
|
|
10
|
+
|
|
11
|
+
**Requires React 19.** Upgrading from 0.x? See [MIGRATION.md](./MIGRATION.md).
|
|
12
|
+
|
|
13
|
+
**Demos:** [Basics](https://codepen.io/oliverwehn/pen/YzMyoBr) (confirm, stacking, async resolvers, programmatic close) · [Animated modals](https://github.com/oliverwehn/react-modal-port/tree/main/demo/codepen-animated) (dialog, drawer, bottom sheet)
|
|
9
14
|
|
|
10
|
-
---
|
|
11
|
-
- [Installation](#-installation)
|
|
12
|
-
- [Components](#-components)
|
|
13
|
-
- [ModalContextProvider](#modalcontextprovider)
|
|
14
|
-
- [ModalPort](#modalport)
|
|
15
|
-
- [Usage Example](#usage-example)
|
|
16
|
-
- [Launching Modals](#-launching-modals)
|
|
17
|
-
- [Stacking Modals And Handling Modal State](#-stacking-modals-and-handling-modal-state)
|
|
18
15
|
---
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
- [Installation](#installation)
|
|
18
|
+
- [Setup](#setup)
|
|
19
|
+
- [Launching modals](#launching-modals)
|
|
20
|
+
- [Asynchronous resolution](#asynchronous-resolution)
|
|
21
|
+
- [Dismissing modals](#dismissing-modals)
|
|
22
|
+
- [Stacking and modal state](#stacking-and-modal-state)
|
|
23
|
+
- [An accessible backdrop with `<dialog>`](#an-accessible-backdrop-with-dialog)
|
|
24
|
+
- [Animating modals in and out](#animating-modals-in-and-out)
|
|
25
|
+
- [API reference](#api-reference)
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
21
28
|
|
|
22
29
|
```bash
|
|
23
|
-
|
|
30
|
+
npm install react-modal-port
|
|
24
31
|
```
|
|
25
32
|
|
|
26
|
-
##
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
Wrap your app in `ModalProvider` and place one `ModalPort` where modals should render. `backdrop` is the component the current modal is rendered into.
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
```tsx
|
|
38
|
+
import type { ReactNode } from 'react';
|
|
39
|
+
import { ModalPort, ModalProvider, type ModalPortRenderProps } from 'react-modal-port';
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
function Backdrop({ children, onBackdropClick }: ModalPortRenderProps) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
onClick={onBackdropClick}
|
|
45
|
+
style={{
|
|
46
|
+
position: 'fixed',
|
|
47
|
+
inset: 0,
|
|
48
|
+
zIndex: 1000,
|
|
49
|
+
display: 'flex',
|
|
50
|
+
alignItems: 'center',
|
|
51
|
+
justifyContent: 'center',
|
|
52
|
+
background: 'rgba(0, 0, 0, 0.65)',
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
31
59
|
|
|
32
|
-
|
|
60
|
+
export function RootLayout({ children }: { children: ReactNode }) {
|
|
61
|
+
return (
|
|
62
|
+
<ModalProvider>
|
|
63
|
+
{children}
|
|
64
|
+
<ModalPort backdrop={Backdrop} />
|
|
65
|
+
</ModalProvider>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
33
69
|
|
|
34
|
-
The
|
|
70
|
+
The package is marked `"use client"`, so it can be imported from React Server Component layouts (for example in Next.js).
|
|
35
71
|
|
|
36
|
-
|
|
72
|
+
## Launching modals
|
|
37
73
|
|
|
38
|
-
|
|
74
|
+
A modal is a plain component. The functions it calls to finish are its **resolvers**.
|
|
39
75
|
|
|
40
|
-
### Usage Example
|
|
41
76
|
```tsx
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
style="
|
|
48
|
-
position: fixed;
|
|
49
|
-
z-index: 1000,
|
|
50
|
-
display: flex;
|
|
51
|
-
justify-content: center;
|
|
52
|
-
align-items-center;
|
|
53
|
-
background-color: rgba(0,0,0,.65);
|
|
54
|
-
top: 0;
|
|
55
|
-
right: 0;
|
|
56
|
-
bottom: 0;
|
|
57
|
-
left: 0;
|
|
58
|
-
"
|
|
59
|
-
onClick={onBackdropClick}
|
|
60
|
-
>
|
|
61
|
-
{children}
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
// Your app’s root layout component
|
|
66
|
-
const RootLayout:React.FC<PropsWithChildren> = ({ children }) => (
|
|
67
|
-
<ModalContextProvider>
|
|
68
|
-
<div style="width: 100%; min-height: 100vh;">
|
|
69
|
-
{children}
|
|
70
|
-
{
|
|
71
|
-
/* The ModalPort defines the place in the app
|
|
72
|
-
* where your modals will be rendered */
|
|
73
|
-
}
|
|
74
|
-
<ModalPort render={Backdrop} />
|
|
75
|
-
</div>
|
|
76
|
-
</ModalContextProvider>
|
|
77
|
-
);
|
|
77
|
+
type DecisionModalProps = {
|
|
78
|
+
question: string;
|
|
79
|
+
decideYay: () => void;
|
|
80
|
+
decideNay: () => void;
|
|
81
|
+
};
|
|
78
82
|
|
|
79
|
-
export
|
|
83
|
+
export function DecisionModal({ question, decideYay, decideNay }: DecisionModalProps) {
|
|
84
|
+
return (
|
|
85
|
+
<div role="dialog" aria-modal="true" aria-labelledby="decision-title">
|
|
86
|
+
<h2 id="decision-title">{question}</h2>
|
|
87
|
+
<button type="button" onClick={decideYay}>Yay</button>
|
|
88
|
+
<button type="button" onClick={decideNay}>Nay</button>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
80
92
|
```
|
|
81
93
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Launching modals makes use of the `useModal` hook that provides you with the `launchModal` function.
|
|
94
|
+
Launch it with `launchModal(Component, resolvers, props?, options?)`:
|
|
85
95
|
|
|
86
96
|
```tsx
|
|
97
|
+
import { useState } from 'react';
|
|
87
98
|
import { useModal } from 'react-modal-port';
|
|
88
|
-
import DecisionModal from './
|
|
89
|
-
|
|
90
|
-
const Page: React.FC = () => {
|
|
99
|
+
import { DecisionModal } from './decision-modal';
|
|
91
100
|
|
|
101
|
+
export function Page() {
|
|
92
102
|
const launchModal = useModal();
|
|
93
|
-
const [
|
|
103
|
+
const [decision, setDecision] = useState<boolean | null>(null);
|
|
94
104
|
|
|
95
|
-
const
|
|
105
|
+
const ask = () => {
|
|
96
106
|
launchModal(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{
|
|
101
|
-
decideYay: () => setDecision(true),
|
|
102
|
-
decideNay: () => setDecision(false),
|
|
103
|
-
// Provide an `onBackdropClick` resolver to have it passed to
|
|
104
|
-
// the Backdrop component
|
|
105
|
-
onBackdropClick: () => {},
|
|
106
|
-
},
|
|
107
|
-
// Optionally, add further props to be passed to the modal component
|
|
108
|
-
{
|
|
109
|
-
timeLeft: Math.random() * 86400
|
|
110
|
-
}
|
|
107
|
+
DecisionModal,
|
|
108
|
+
{ decideYay: () => setDecision(true), decideNay: () => setDecision(false) },
|
|
109
|
+
{ question: 'Ship it?' },
|
|
111
110
|
);
|
|
112
|
-
}
|
|
111
|
+
};
|
|
113
112
|
|
|
114
113
|
return (
|
|
115
|
-
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
) || (
|
|
120
|
-
<p>Your decision is: {decision && "Yay" || "Nay"}</p>
|
|
121
|
-
)}
|
|
122
|
-
|
|
123
|
-
<div style="display: flex; flex-direction: row; gap: 0.5rem;">
|
|
124
|
-
<button type="button" onClick={onClick}>Let me decide now</button>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
114
|
+
<>
|
|
115
|
+
<p>{decision === null ? 'Make your decision!' : `Your decision: ${decision ? 'Yay' : 'Nay'}`}</p>
|
|
116
|
+
<button type="button" onClick={ask}>Decide now</button>
|
|
117
|
+
</>
|
|
127
118
|
);
|
|
128
119
|
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
| Argument | Description |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `Component` | The modal component. Its props define what the other arguments must contain. |
|
|
125
|
+
| `resolvers` | Function props of the modal that close it. Each one is called with the modal's arguments, and the modal is removed once it returns (or its promise resolves). Calling a second resolver after the first is ignored. |
|
|
126
|
+
| `props` | The modal's remaining props. Required when the modal still has required props, otherwise optional. |
|
|
127
|
+
| `options` | `{ onDismiss }`: see [Dismissing modals](#dismissing-modals). |
|
|
129
128
|
|
|
129
|
+
TypeScript catches the common mistakes:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
launchModal(DecisionModal, { decideYey: () => {} }, { question: '?' }); // ✗ unknown resolver
|
|
133
|
+
launchModal(DecisionModal, { decideYay: () => {}, decideNay: () => {} }); // ✗ `question` is missing
|
|
130
134
|
```
|
|
131
|
-
The `launchModal` function expects the following arguments:
|
|
132
|
-
| Argument | Type | Description |
|
|
133
|
-
|----------|------|-------------|
|
|
134
|
-
| render | `React.FC` | A function that renders the modal and returns a `ReactNode`. |
|
|
135
|
-
| resolvers | `{ [key: string]: (...args: any[]) => Promise<void>\|void }` | An object of resolver functions that will be passed as props to `render`. |
|
|
136
|
-
| props | `{ [key: string]: any }` | Optional: Further props you’d like to pass to the modal component in `render`. |
|
|
137
135
|
|
|
138
|
-
|
|
136
|
+
A function prop that is *not* listed in `resolvers` can be passed in `props`. It is then just a callback and does not close the modal.
|
|
137
|
+
|
|
138
|
+
`launchModal` returns a handle, `{ id, close() }`. `close()` removes that modal without calling a resolver, for example after a timeout.
|
|
139
|
+
|
|
140
|
+
## Asynchronous resolution
|
|
141
|
+
|
|
142
|
+
If a resolver returns a promise, the modal stays open until it settles:
|
|
139
143
|
|
|
140
144
|
```tsx
|
|
145
|
+
launchModal(SaveModal, {
|
|
146
|
+
onSave: async (draft: Draft) => {
|
|
147
|
+
await api.save(draft); // the modal is still shown while this runs
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
```
|
|
141
151
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
decideNay: () => void;
|
|
145
|
-
leaveMeAlone?: () => void;
|
|
146
|
-
};
|
|
152
|
+
- **Fulfilled:** the modal closes. This happens even if other modals were stacked on top of it in the meantime; only *this* modal is removed.
|
|
153
|
+
- **Rejected or thrown:** the modal stays open and the promise returned to the modal component rejects with the same error, so the modal can show it and let the user retry. Resolvers can be called again after a failure.
|
|
147
154
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}) => (
|
|
152
|
-
<div
|
|
153
|
-
style="background-color: white; border-radius: 0.5rem; padding: 1rem; display: flex; flex-direction: column; align-items: center; gap: 0.5rem;"
|
|
154
|
-
role="dialog"
|
|
155
|
-
aria-modal="true"
|
|
156
|
-
aria-labeledby="modal-title"
|
|
157
|
-
aria-describedby="modal-description"
|
|
158
|
-
>
|
|
159
|
-
<h4 id="modal-title">How do you decide?</h4>
|
|
160
|
-
<div id ="modal-description">
|
|
161
|
-
<p>Make your decision and click one of the buttons below.</p>
|
|
162
|
-
</div>
|
|
163
|
-
<div style="display: flex; flex-direction: row; gap: 0.25rem; justify-content: center;">
|
|
164
|
-
<button type="button" onClick={decideYay}>Yay</button>
|
|
165
|
-
<button type="button" onClick={decideNay}>Nay</button>
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
)
|
|
155
|
+
## Dismissing modals
|
|
156
|
+
|
|
157
|
+
Pass `onDismiss` in the options to make a modal dismissible from outside its content:
|
|
169
158
|
|
|
170
|
-
|
|
159
|
+
```tsx
|
|
160
|
+
launchModal(DecisionModal, resolvers, { question: 'Ship it?' }, { onDismiss: () => setDecision(null) });
|
|
171
161
|
```
|
|
172
162
|
|
|
173
|
-
|
|
163
|
+
The backdrop then receives `onBackdropClick`, which calls `onDismiss` (and closes the modal) only for clicks on the backdrop itself, not for clicks that bubble up from the modal. For modals launched without `onDismiss`, `onBackdropClick` is `undefined`, so the backdrop can tell whether the current modal is dismissible.
|
|
164
|
+
|
|
165
|
+
## Stacking and modal state
|
|
166
|
+
|
|
167
|
+
Modals launched while another one is open are stacked. The port shows the top one and returns to the previous one when it closes. Each stacked modal keeps its own **modal state**, read and updated with `useModalState()` from inside the modal. Modal state survives while other modals are on top and is discarded when the modal closes.
|
|
174
168
|
|
|
175
|
-
|
|
169
|
+
Component state (`useState`) inside a modal does **not** survive being covered, because only the top modal is mounted. Use modal state for anything that should outlive a nested modal.
|
|
176
170
|
|
|
177
|
-
So you could adapt the `onClick` handler in our earlier example as follows:
|
|
178
171
|
```tsx
|
|
179
|
-
|
|
172
|
+
import { useModal, useModalState } from 'react-modal-port';
|
|
173
|
+
import { ConfirmModal } from './confirm-modal';
|
|
180
174
|
|
|
181
|
-
|
|
175
|
+
type AskForNameProps = { provideName: (name: string) => void };
|
|
176
|
+
|
|
177
|
+
export function AskForNameModal({ provideName }: AskForNameProps) {
|
|
178
|
+
const launchModal = useModal();
|
|
179
|
+
const [state, setState] = useModalState<{ name: string }>();
|
|
180
|
+
const name = state?.name ?? '';
|
|
181
|
+
|
|
182
|
+
const confirm = () => {
|
|
182
183
|
launchModal(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
decideYay: () => setDecision(true),
|
|
188
|
-
decideNay: () => setDecision(false),
|
|
189
|
-
letMeThinkAboutIt: (p: Promise) => p,
|
|
190
|
-
leaveMeAlone: () => {},
|
|
191
|
-
},
|
|
184
|
+
ConfirmModal,
|
|
185
|
+
// Resolving the confirmation also resolves this modal.
|
|
186
|
+
{ confirm: (ok: boolean) => { if (ok) provideName(name); } },
|
|
187
|
+
{ name },
|
|
192
188
|
);
|
|
193
|
-
}
|
|
189
|
+
};
|
|
194
190
|
|
|
195
|
-
|
|
191
|
+
return (
|
|
192
|
+
<div role="dialog" aria-modal="true" aria-labelledby="name-title">
|
|
193
|
+
<h2 id="name-title">How should we call you?</h2>
|
|
194
|
+
<input
|
|
195
|
+
aria-label="Name"
|
|
196
|
+
value={name}
|
|
197
|
+
onChange={(event) => {
|
|
198
|
+
const value = event.target.value;
|
|
199
|
+
setState((prev) => ({ ...prev, name: value }));
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
<button type="button" onClick={confirm}>Set name</button>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
196
206
|
```
|
|
197
207
|
|
|
198
|
-
|
|
208
|
+
`setState` accepts a new state object or an updater function, like React's own `setState`. Prefer the updater form when the new state depends on the old one.
|
|
209
|
+
|
|
210
|
+
## An accessible backdrop with `<dialog>`
|
|
199
211
|
|
|
212
|
+
The library leaves markup and accessibility to you, so they can match your design system. The native `<dialog>` element covers most of it: shown with `showModal()`, it sits in the top layer, makes the rest of the page inert, traps focus and fires `cancel` on Escape.
|
|
200
213
|
|
|
201
214
|
```tsx
|
|
215
|
+
import { useEffect, useRef } from 'react';
|
|
216
|
+
import type { ModalPortRenderProps } from 'react-modal-port';
|
|
202
217
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
decideNay: () => void;
|
|
206
|
-
letMeThinkAboutIt: (p: Promise) => Promise;
|
|
207
|
-
leaveMeAlone?: () => void;
|
|
208
|
-
};
|
|
218
|
+
export function DialogBackdrop({ children, onBackdropClick }: ModalPortRenderProps) {
|
|
219
|
+
const ref = useRef<HTMLDialogElement>(null);
|
|
209
220
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
const imUndecided = () => {
|
|
217
|
-
const p = new Promise((resolve) => {
|
|
218
|
-
const options = [ decideYay, decideNay ];
|
|
219
|
-
setTimeout(
|
|
220
|
-
() => resolve(options[Math.round(Math.random())]),
|
|
221
|
-
Math.random() * 60000
|
|
222
|
-
);
|
|
223
|
-
});
|
|
224
|
-
letMeThinkAboutIt(p);
|
|
225
|
-
};
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
const dialog = ref.current;
|
|
223
|
+
dialog?.showModal();
|
|
224
|
+
return () => dialog?.close();
|
|
225
|
+
}, []);
|
|
226
226
|
|
|
227
227
|
return (
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
228
|
+
<dialog
|
|
229
|
+
ref={ref}
|
|
230
|
+
className="modal-backdrop"
|
|
231
|
+
onClick={onBackdropClick}
|
|
232
|
+
onCancel={(event) => {
|
|
233
|
+
event.preventDefault(); // let the library decide whether the modal closes
|
|
234
|
+
onBackdropClick?.(event);
|
|
235
|
+
}}
|
|
234
236
|
>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
<p>Make your decision and click one of the buttons below.</p>
|
|
238
|
-
</div>
|
|
239
|
-
<div style="display: flex; flex-direction: row; gap: 0.25rem; justify-content: center;">
|
|
240
|
-
<button type="button" onClick={decideYay}>Yay</button>
|
|
241
|
-
<button type="button" onClick={decideNay}>Nay</button>
|
|
242
|
-
<button type="button" onClick={imUndecided}>Let me think</button>
|
|
243
|
-
</div>
|
|
244
|
-
</div>
|
|
237
|
+
{children}
|
|
238
|
+
</dialog>
|
|
245
239
|
);
|
|
246
240
|
}
|
|
241
|
+
```
|
|
247
242
|
|
|
248
|
-
|
|
243
|
+
```css
|
|
244
|
+
/* Lock page scroll while a modal is open */
|
|
245
|
+
body:has(dialog.modal-backdrop[open]) { overflow: hidden; }
|
|
249
246
|
```
|
|
250
247
|
|
|
251
|
-
|
|
248
|
+
Give each modal an accessible name (`aria-labelledby` or `aria-label`), and move focus into it if the first focusable element is not the right target.
|
|
249
|
+
|
|
250
|
+
## Animating modals in and out
|
|
252
251
|
|
|
253
|
-
|
|
252
|
+
**Enter:** every modal remounts when it becomes the top of the stack, so a CSS animation on the modal's root element plays each time it appears, including when it reappears after a modal on top of it closes.
|
|
254
253
|
|
|
255
|
-
|
|
254
|
+
```css
|
|
255
|
+
.modal { animation: pop-in 250ms ease-out both; }
|
|
256
|
+
@keyframes pop-in { from { opacity: 0; transform: scale(0.96); } }
|
|
257
|
+
```
|
|
256
258
|
|
|
257
|
-
|
|
259
|
+
**Exit:** a modal stays mounted until its resolver settles, so a resolver that first awaits an exit animation gets a clean exit animation. A small wrapper around `launchModal` applies this to every resolver and to `onDismiss`:
|
|
258
260
|
|
|
259
261
|
```tsx
|
|
260
|
-
import
|
|
261
|
-
import ConfirmModal from "./ConfirmModal";
|
|
262
|
+
import { useModal, type LaunchModal } from 'react-modal-port';
|
|
262
263
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
};
|
|
264
|
+
// Plays the exit animation of the modal with this id, if it is the one on screen.
|
|
265
|
+
declare function playExit(modalId: number): Promise<void>;
|
|
266
266
|
|
|
267
|
-
|
|
268
|
-
provideName,
|
|
269
|
-
}) => {
|
|
267
|
+
type AnyFn = (...args: never[]) => unknown;
|
|
270
268
|
|
|
269
|
+
export function useAnimatedModal(): LaunchModal {
|
|
271
270
|
const launchModal = useModal();
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
271
|
+
return ((Component, resolvers, props, options = {}) => {
|
|
272
|
+
let id = 0;
|
|
273
|
+
const animated =
|
|
274
|
+
<F extends AnyFn>(fn: F) =>
|
|
275
|
+
async (...args: Parameters<F>) => {
|
|
276
|
+
await playExit(id);
|
|
277
|
+
return fn(...args);
|
|
278
|
+
};
|
|
279
|
+
const wrapped = Object.fromEntries(
|
|
280
|
+
Object.entries(resolvers as Record<string, AnyFn>).map(([key, fn]) => [key, animated(fn)]),
|
|
281
|
+
);
|
|
282
|
+
const handle = launchModal(Component, wrapped as never, props as never, {
|
|
283
|
+
...options,
|
|
284
|
+
onDismiss: options.onDismiss && animated(options.onDismiss),
|
|
277
285
|
});
|
|
278
|
-
|
|
286
|
+
id = handle.id;
|
|
287
|
+
return { id, close: animated(handle.close) };
|
|
288
|
+
}) as LaunchModal;
|
|
289
|
+
}
|
|
290
|
+
```
|
|
279
291
|
|
|
280
|
-
|
|
281
|
-
launchModal(
|
|
282
|
-
ConfirmModal,
|
|
283
|
-
{
|
|
284
|
-
// Chain first modal’s resolution to the second one’s
|
|
285
|
-
confirm: () => provideName(modalState.name),
|
|
286
|
-
},
|
|
287
|
-
// Pass the name on to the ConfirmModal for rendering
|
|
288
|
-
{
|
|
289
|
-
name: modalState.name,
|
|
290
|
-
}
|
|
291
|
-
);
|
|
292
|
-
};
|
|
292
|
+
The backdrop receives `modalId` and `stackSize`, so `playExit` can check that the modal is the one on screen, and fade the backdrop out too when it is the last one. The [animated demo](https://github.com/oliverwehn/react-modal-port/tree/main/demo/codepen-animated) has a complete version using the Web Animations API, with drawer and bottom-sheet variants and support for `prefers-reduced-motion`.
|
|
293
293
|
|
|
294
|
-
|
|
295
|
-
<div
|
|
296
|
-
style="background-color: white; border-radius: 0.5rem; padding: 1rem; display: flex; flex-direction: column; align-items: center; gap: 0.5rem;"
|
|
297
|
-
role="dialog"
|
|
298
|
-
aria-modal="true"
|
|
299
|
-
aria-labeledby="modal-title"
|
|
300
|
-
aria-describedby="modal-description"
|
|
301
|
-
>
|
|
302
|
-
<h4 id="modal-title">How should we call you?</h4>
|
|
303
|
-
<div id="modal-description">
|
|
304
|
-
<p>Please enter your firstname or nickname of choice.</p>
|
|
305
|
-
</div>
|
|
306
|
-
<input type="text" value={modalState.name} onChange={onNameChange} style="width: 100%">
|
|
307
|
-
<button type="button" onClick={onButtonClick}>Set name</button>
|
|
308
|
-
</div>
|
|
309
|
-
);
|
|
294
|
+
## API reference
|
|
310
295
|
|
|
311
|
-
|
|
296
|
+
### `<ModalProvider>`
|
|
297
|
+
|
|
298
|
+
Holds the modal stack. Wrap your app (or the part that uses modals) in it. `ModalContextProvider` is a deprecated alias.
|
|
299
|
+
|
|
300
|
+
### `<ModalPort>`
|
|
301
|
+
|
|
302
|
+
| Prop | Type | Description |
|
|
303
|
+
| --- | --- | --- |
|
|
304
|
+
| `backdrop` | `ComponentType<ModalPortRenderProps>` | Renders around the current modal. Receives `children`, `onBackdropClick` (undefined unless the modal is dismissible), `modalId` and `stackSize`. Optional. |
|
|
305
|
+
| `container` | `Element \| DocumentFragment` | Render into this element through a portal. |
|
|
306
|
+
| `onModalLaunch` | `() => void` | The stack went from empty to non-empty. |
|
|
307
|
+
| `onModalClose` | `() => void` | The stack became empty. |
|
|
308
|
+
| `onStackChange` | `(size: number) => void` | Any modal was launched or closed. |
|
|
309
|
+
| `render` | | Deprecated alias of `backdrop`. |
|
|
310
|
+
|
|
311
|
+
### Hooks
|
|
312
|
+
|
|
313
|
+
| Hook | Returns |
|
|
314
|
+
| --- | --- |
|
|
315
|
+
| `useModal()` | `launchModal`. Stable across renders; components using only this hook do not re-render when modals open or close. |
|
|
316
|
+
| `useModalState<S>()` | `[state \| null, setState]` for the modal the hook is called in. Called outside a modal, it refers to the top modal (`null` if none). |
|
|
317
|
+
| `useModalStack()` | A read-only array of `{ id, render, props, state }`, bottom first. |
|
|
318
|
+
| `useModalContext()` | Deprecated. `{ stack, launchModal, updateState }`; re-renders on every stack change. |
|
|
319
|
+
|
|
320
|
+
All hooks throw if used outside a `ModalProvider`.
|
|
321
|
+
|
|
322
|
+
## License
|
|
312
323
|
|
|
313
|
-
|
|
324
|
+
MIT
|
package/dist/index.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{jsx as r,Fragment as n}from"react/jsx-runtime";import{createContext as t,useState as e,useCallback as o,useContext as l,useMemo as u,useRef as c,useEffect as a}from"react";function i(){return i=Object.assign?Object.assign.bind():function(r){for(var n=1;n<arguments.length;n++){var t=arguments[n];for(var e in t)({}).hasOwnProperty.call(t,e)&&(r[e]=t[e])}return r},i.apply(null,arguments)}var s=t(null),d=function(n){var t=n.children,l=e([]),u=l[0],c=l[1],a=o(function(r,n,t){void 0===t&&(t={});var e=Object.keys(n).reduce(function(r,t){var e=n[t];return r[t]=function(){try{return Promise.resolve(e.apply(void 0,[].slice.call(arguments))).then(function(r){return c(function(r){return r.slice(0,-1)}),r})}catch(r){return Promise.reject(r)}},r},{});c(function(n){return[].concat(n,[{render:r,resolvers:e,props:t,state:{}}])})},[]),d=o(function(r){c(function(n){var t=[].concat(n);return t.length>0&&(t[t.length-1]=i({},t[t.length-1],{state:i({},r)})),t})},[]);return r(s.Provider,{value:{stack:u,launchModal:a,updateStack:c,updateState:d},children:t})};function v(){var r=l(s);if(!r)throw new Error("useModalContext must be used within a ModalContextProvider");return r}function f(){return v().launchModal}function h(){var r=v(),n=r.stack,t=r.updateState;return 0===n.length?[null,t]:[n[n.length-1].state,t]}var p=function(t){var e=t.onModalLaunch,l=t.onModalClose,s=t.render,d=v().stack,f=u(function(){return d.length>0?d[d.length-1]:null},[d]),h=c(null),p=o(function(r){r.currentTarget===r.target&&null!=f&&f.resolvers.onBackdropClick&&f.resolvers.onBackdropClick(r)},[f]);a(function(){var r=h.current;!r&&f?null==e||e():r&&!f&&(null==l||l()),h.current=f},[f,e,l]);var g=null==f?void 0:f.render;return r(n,{children:g&&r(s,{onBackdropClick:p,children:r(g,i({},f.resolvers,f.props))})||null})};export{d as ModalContextProvider,p as ModalPort,f as useModal,v as useModalContext,h as useModalState};
|
|
2
|
-
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":["../src/context.tsx","../src/modal-port.tsx"],"sourcesContent":["import { \n createContext, \n useContext, \n useState, \n useCallback,\n type SyntheticEvent \n} from 'react';\nimport { \n type ModalStackItem,\n type ModalContextProperties,\n type LaunchModal,\n type LaunchModalResolvers,\n type ModalState,\n type UpdateModalState,\n ModalProps,\n} from './types';\n\nexport const ModalContext = createContext<ModalContextProperties | null>(null);\n\nexport const ModalContextProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {\n\n const [ stack, updateStack ] = useState<ModalStackItem<any>[]>([]);\n\n const launchModal = useCallback(<P extends Record<string, any> = Record<string, any>>(\n render: React.FunctionComponent<ModalProps<P>>, \n resolvers: LaunchModalResolvers,\n props: any = {}\n ) => {\n // Wrap resolvers to update the stack\n const wrappedResovers = Object.keys(resolvers).reduce((acc, key) => {\n const resolver = resolvers[key];\n acc[key] = async (...args: any[]) => {\n const result = await resolver(...args);\n // update stack with an array containing all the items in the stack except the last one\n updateStack(currStack => currStack.slice(0, -1));\n return result;\n };\n return acc;\n }, {} as LaunchModalResolvers);\n\n updateStack(prevStack => [\n ...prevStack,\n {\n render,\n resolvers: wrappedResovers,\n props,\n state: {},\n },\n ]);\n }, []);\n\n const updateState = useCallback((newState: ModalState) => {\n updateStack(prevStack => {\n const updatedStack = [...prevStack];\n if (updatedStack.length > 0) {\n updatedStack[updatedStack.length - 1] = {\n ...updatedStack[updatedStack.length - 1],\n state: { ...newState }\n };\n }\n return updatedStack;\n });\n }, []);\n\n const contextProperties: ModalContextProperties = {\n stack,\n launchModal,\n updateStack,\n updateState,\n };\n\n return <ModalContext.Provider value={contextProperties}>{children}</ModalContext.Provider>;\n\n};\n\n/**\n * Get properties from the modal context\n * @returns {ModalContextProperties} Object with the modal context properties\n */\nexport function useModalContext(): ModalContextProperties {\n const context = useContext(ModalContext);\n if (!context) {\n throw new Error('useModalContext must be used within a ModalContextProvider');\n }\n return context;\n}\n\n/**\n * Short-hand for `useModalContext().launchModal`\n * @returns {LaunchModal} The launchModal function\n */\nexport function useModal() {\n const { launchModal } = useModalContext();\n return launchModal;\n}\n\n/**\n * Get access to the current modal state and a function to update it\n * @returns {[ ModalState | null, UpdateModalState ]} A tuple with the current modal state and a function to update it\n */\nexport function useModalState(): [ ModalState | null, UpdateModalState ] {\n const { stack, updateState } = useModalContext();\n if (stack.length === 0) {\n return [null, updateState];\n }\n const state = stack[stack.length - 1].state;\n return [ state, updateState ];\n}\n","import { \n ModalProps,\n type ModalPortProps, \n type ModalStackItem \n} from \"./types\";\nimport { useModalContext } from \"./context\";\nimport { \n type SyntheticEvent, \n useCallback, \n useEffect, \n useMemo,\n useRef,\n type ReactElement\n} from \"react\";\n\nconst ModalPort: React.FC<ModalPortProps> = ({\n onModalLaunch,\n onModalClose,\n render: Backdrop\n}) => {\n\n const { stack } = useModalContext();\n const currentModal: ModalStackItem<ModalProps> | null = useMemo(\n () => stack.length > 0 ? stack[stack.length - 1] : null,\n [ stack ]\n );\n \n const previousModalRef = useRef<ModalStackItem<ModalProps> | null>(null);\n \n const onBackdropClick = useCallback((ev: SyntheticEvent) => {\n if (ev.currentTarget !== ev.target) return;\n if (currentModal?.resolvers.onBackdropClick) {\n currentModal.resolvers.onBackdropClick(ev);\n }\n }, [ currentModal ]);\n\n useEffect(() => {\n const previousModal = previousModalRef.current;\n \n // Modal was launched (went from no modal to having a modal)\n if (!previousModal && currentModal) {\n onModalLaunch?.();\n }\n // Modal was closed (went from having a modal to no modal)\n else if (previousModal && !currentModal) {\n onModalClose?.();\n }\n \n // Update the ref for next time\n previousModalRef.current = currentModal;\n }, [ currentModal, onModalLaunch, onModalClose ]);\n\n const ModalContent = currentModal?.render;\n\n return (\n <>\n {ModalContent && (\n <Backdrop onBackdropClick={onBackdropClick}>\n <ModalContent\n {...currentModal.resolvers}\n {...currentModal.props}\n />\n </Backdrop>\n ) || null}\n </>\n );\n};\n\nexport default ModalPort;\nexport { ModalPort };\n"],"names":["ModalContext","createContext","ModalContextProvider","_ref","children","_useState","useState","stack","updateStack","launchModal","useCallback","render","resolvers","props","wrappedResovers","Object","keys","reduce","acc","key","resolver","Promise","resolve","apply","slice","call","arguments","then","result","currStack","e","reject","prevStack","concat","state","updateState","newState","updatedStack","length","_extends","_jsx","Provider","value","useModalContext","context","useContext","Error","useModal","useModalState","_useModalContext2","ModalPort","onModalLaunch","onModalClose","Backdrop","currentModal","useMemo","previousModalRef","useRef","onBackdropClick","ev","currentTarget","target","useEffect","previousModal","current","ModalContent","_Fragment"],"mappings":"+YAiBaA,EAAeC,EAA6C,MAE5DC,EAAuB,SAAHC,GAA6D,IAAvDC,EAAQD,EAARC,SAErCC,EAA+BC,EAAgC,IAAvDC,EAAKF,EAAA,GAAEG,EAAWH,EAE1B,GAAMI,EAAcC,EAAY,SAC9BC,EACAC,EACAC,QAAAA,IAAAA,IAAAA,EAAa,IAGb,IAAMC,EAAkBC,OAAOC,KAAKJ,GAAWK,OAAO,SAACC,EAAKC,GAC1D,IAAMC,EAAWR,EAAUO,GAO3B,OANAD,EAAIC,GAAI,WAAA,IAA4B,OAAAE,QAAAC,QACbF,EAAQG,WAAAC,EAAAA,GAAAA,MAAAC,KADKC,aACIC,KAAhCC,SAAAA,GAGN,OADApB,EAAY,SAAAqB,GAAa,OAAAA,EAAUL,MAAM,GAAI,EAAE,GACxCI,CAAO,EAChB,CAAC,MAAAE,UAAAT,QAAAU,OAAAD,EAAA,CAAA,EACMZ,CACT,EAAG,CAAA,GAEHV,EAAY,SAAAwB,GAAS,MAAA,GAAAC,OAChBD,EACH,CAAA,CACErB,OAAAA,EACAC,UAAWE,EACXD,MAAAA,EACAqB,MAAO,CAAA,IAEV,EACH,EAAG,IAEGC,EAAczB,EAAY,SAAC0B,GAC/B5B,EAAY,SAAAwB,GACV,IAAMK,EAAYJ,GAAAA,OAAOD,GAOzB,OANIK,EAAaC,OAAS,IACxBD,EAAaA,EAAaC,OAAS,GAAEC,EAChCF,CAAAA,EAAAA,EAAaA,EAAaC,OAAS,GACtCJ,CAAAA,MAAKK,KAAOH,MAGTC,CACT,EACF,EAAG,IASH,OAAOG,EAACxC,EAAayC,SAAQ,CAACC,MAPoB,CAChDnC,MAAAA,EACAE,YAAAA,EACAD,YAAAA,EACA2B,YAAAA,GAGoD/B,SAAGA,GAE3D,EAMgB,SAAAuC,IACd,IAAMC,EAAUC,EAAW7C,GAC3B,IAAK4C,EACH,MAAU,IAAAE,MAAM,8DAElB,OAAOF,CACT,UAMgBG,IAEd,OADwBJ,IAAhBlC,WAEV,UAMgBuC,IACd,IAAAC,EAA+BN,IAAvBpC,EAAK0C,EAAL1C,MAAO4B,EAAWc,EAAXd,YACf,OAAqB,IAAjB5B,EAAM+B,OACD,CAAC,KAAMH,GAGT,CADO5B,EAAMA,EAAM+B,OAAS,GAAGJ,MACtBC,EAClB,CC5FM,IAAAe,EAAsC,SAA7B/C,GACb,IAAAgD,EAAahD,EAAbgD,cACAC,EAAYjD,EAAZiD,aACQC,EAAQlD,EAAhBQ,OAGQJ,EAAUoC,IAAVpC,MACF+C,EAAkDC,EACtD,WAAA,OAAMhD,EAAM+B,OAAS,EAAI/B,EAAMA,EAAM+B,OAAS,GAAK,IAAI,EACvD,CAAE/B,IAGEiD,EAAmBC,EAA0C,MAE7DC,EAAkBhD,EAAY,SAACiD,GAC/BA,EAAGC,gBAAkBD,EAAGE,cACxBP,GAAAA,EAAc1C,UAAU8C,iBAC1BJ,EAAa1C,UAAU8C,gBAAgBC,EAE3C,EAAG,CAAEL,IAELQ,EAAU,WACR,IAAMC,EAAgBP,EAAiBQ,SAGlCD,GAAiBT,EACP,MAAbH,GAAAA,IAGOY,IAAkBT,IACzBF,MAAAA,GAAAA,KAIFI,EAAiBQ,QAAUV,CAC7B,EAAG,CAAEA,EAAcH,EAAeC,IAElC,IAAMa,EAAeX,MAAAA,OAAAA,EAAAA,EAAc3C,OAEnC,OACE6B,EAAA0B,EAAA,CAAA9D,SACG6D,GACCzB,EAACa,EAAQ,CAACK,gBAAiBA,EAAetD,SACxCoC,EAACyB,EAAY1B,EACPe,CAAAA,EAAAA,EAAa1C,UACb0C,EAAazC,WAGlB,MAGX"}
|
package/dist/index.modern.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{jsx as n,Fragment as t}from"react/jsx-runtime";import{createContext as r,useState as e,useCallback as o,useContext as l,useMemo as c,useRef as s,useEffect as a}from"react";function u(){return u=Object.assign?Object.assign.bind():function(n){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var e in r)({}).hasOwnProperty.call(r,e)&&(n[e]=r[e])}return n},u.apply(null,arguments)}const i=r(null),d=({children:t})=>{const[r,l]=e([]),c=o((n,t,r={})=>{const e=Object.keys(t).reduce((n,r)=>{const e=t[r];return n[r]=async(...n)=>{const t=await e(...n);return l(n=>n.slice(0,-1)),t},n},{});l(t=>[...t,{render:n,resolvers:e,props:r,state:{}}])},[]),s=o(n=>{l(t=>{const r=[...t];return r.length>0&&(r[r.length-1]=u({},r[r.length-1],{state:u({},n)})),r})},[]);return n(i.Provider,{value:{stack:r,launchModal:c,updateStack:l,updateState:s},children:t})};function h(){const n=l(i);if(!n)throw new Error("useModalContext must be used within a ModalContextProvider");return n}function p(){const{launchModal:n}=h();return n}function g(){const{stack:n,updateState:t}=h();return 0===n.length?[null,t]:[n[n.length-1].state,t]}const k=({onModalLaunch:r,onModalClose:e,render:l})=>{const{stack:i}=h(),d=c(()=>i.length>0?i[i.length-1]:null,[i]),p=s(null),g=o(n=>{n.currentTarget===n.target&&null!=d&&d.resolvers.onBackdropClick&&d.resolvers.onBackdropClick(n)},[d]);a(()=>{const n=p.current;!n&&d?null==r||r():n&&!d&&(null==e||e()),p.current=d},[d,r,e]);const k=null==d?void 0:d.render;return n(t,{children:k&&n(l,{onBackdropClick:g,children:n(k,u({},d.resolvers,d.props))})||null})};export{d as ModalContextProvider,k as ModalPort,p as useModal,h as useModalContext,g as useModalState};
|
|
2
|
-
//# sourceMappingURL=index.modern.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.modern.mjs","sources":["../src/context.tsx","../src/modal-port.tsx"],"sourcesContent":["import { \n createContext, \n useContext, \n useState, \n useCallback,\n type SyntheticEvent \n} from 'react';\nimport { \n type ModalStackItem,\n type ModalContextProperties,\n type LaunchModal,\n type LaunchModalResolvers,\n type ModalState,\n type UpdateModalState,\n ModalProps,\n} from './types';\n\nexport const ModalContext = createContext<ModalContextProperties | null>(null);\n\nexport const ModalContextProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {\n\n const [ stack, updateStack ] = useState<ModalStackItem<any>[]>([]);\n\n const launchModal = useCallback(<P extends Record<string, any> = Record<string, any>>(\n render: React.FunctionComponent<ModalProps<P>>, \n resolvers: LaunchModalResolvers,\n props: any = {}\n ) => {\n // Wrap resolvers to update the stack\n const wrappedResovers = Object.keys(resolvers).reduce((acc, key) => {\n const resolver = resolvers[key];\n acc[key] = async (...args: any[]) => {\n const result = await resolver(...args);\n // update stack with an array containing all the items in the stack except the last one\n updateStack(currStack => currStack.slice(0, -1));\n return result;\n };\n return acc;\n }, {} as LaunchModalResolvers);\n\n updateStack(prevStack => [\n ...prevStack,\n {\n render,\n resolvers: wrappedResovers,\n props,\n state: {},\n },\n ]);\n }, []);\n\n const updateState = useCallback((newState: ModalState) => {\n updateStack(prevStack => {\n const updatedStack = [...prevStack];\n if (updatedStack.length > 0) {\n updatedStack[updatedStack.length - 1] = {\n ...updatedStack[updatedStack.length - 1],\n state: { ...newState }\n };\n }\n return updatedStack;\n });\n }, []);\n\n const contextProperties: ModalContextProperties = {\n stack,\n launchModal,\n updateStack,\n updateState,\n };\n\n return <ModalContext.Provider value={contextProperties}>{children}</ModalContext.Provider>;\n\n};\n\n/**\n * Get properties from the modal context\n * @returns {ModalContextProperties} Object with the modal context properties\n */\nexport function useModalContext(): ModalContextProperties {\n const context = useContext(ModalContext);\n if (!context) {\n throw new Error('useModalContext must be used within a ModalContextProvider');\n }\n return context;\n}\n\n/**\n * Short-hand for `useModalContext().launchModal`\n * @returns {LaunchModal} The launchModal function\n */\nexport function useModal() {\n const { launchModal } = useModalContext();\n return launchModal;\n}\n\n/**\n * Get access to the current modal state and a function to update it\n * @returns {[ ModalState | null, UpdateModalState ]} A tuple with the current modal state and a function to update it\n */\nexport function useModalState(): [ ModalState | null, UpdateModalState ] {\n const { stack, updateState } = useModalContext();\n if (stack.length === 0) {\n return [null, updateState];\n }\n const state = stack[stack.length - 1].state;\n return [ state, updateState ];\n}\n","import { \n ModalProps,\n type ModalPortProps, \n type ModalStackItem \n} from \"./types\";\nimport { useModalContext } from \"./context\";\nimport { \n type SyntheticEvent, \n useCallback, \n useEffect, \n useMemo,\n useRef,\n type ReactElement\n} from \"react\";\n\nconst ModalPort: React.FC<ModalPortProps> = ({\n onModalLaunch,\n onModalClose,\n render: Backdrop\n}) => {\n\n const { stack } = useModalContext();\n const currentModal: ModalStackItem<ModalProps> | null = useMemo(\n () => stack.length > 0 ? stack[stack.length - 1] : null,\n [ stack ]\n );\n \n const previousModalRef = useRef<ModalStackItem<ModalProps> | null>(null);\n \n const onBackdropClick = useCallback((ev: SyntheticEvent) => {\n if (ev.currentTarget !== ev.target) return;\n if (currentModal?.resolvers.onBackdropClick) {\n currentModal.resolvers.onBackdropClick(ev);\n }\n }, [ currentModal ]);\n\n useEffect(() => {\n const previousModal = previousModalRef.current;\n \n // Modal was launched (went from no modal to having a modal)\n if (!previousModal && currentModal) {\n onModalLaunch?.();\n }\n // Modal was closed (went from having a modal to no modal)\n else if (previousModal && !currentModal) {\n onModalClose?.();\n }\n \n // Update the ref for next time\n previousModalRef.current = currentModal;\n }, [ currentModal, onModalLaunch, onModalClose ]);\n\n const ModalContent = currentModal?.render;\n\n return (\n <>\n {ModalContent && (\n <Backdrop onBackdropClick={onBackdropClick}>\n <ModalContent\n {...currentModal.resolvers}\n {...currentModal.props}\n />\n </Backdrop>\n ) || null}\n </>\n );\n};\n\nexport default ModalPort;\nexport { ModalPort };\n"],"names":["ModalContext","createContext","ModalContextProvider","children","stack","updateStack","useState","launchModal","useCallback","render","resolvers","props","wrappedResovers","Object","keys","reduce","acc","key","resolver","async","args","result","currStack","slice","prevStack","state","updateState","newState","updatedStack","length","_extends","_jsx","Provider","value","useModalContext","context","useContext","Error","useModal","useModalState","ModalPort","onModalLaunch","onModalClose","Backdrop","currentModal","useMemo","previousModalRef","useRef","onBackdropClick","ev","currentTarget","target","useEffect","previousModal","current","ModalContent","_Fragment"],"mappings":"iZAiBaA,EAAeC,EAA6C,MAE5DC,EAAuBA,EAAGC,eAErC,MAAQC,EAAOC,GAAgBC,EAAgC,IAEzDC,EAAcC,EAAY,CAC9BC,EACAC,EACAC,EAAa,CAAE,KAGf,MAAMC,EAAkBC,OAAOC,KAAKJ,GAAWK,OAAO,CAACC,EAAKC,KAC1D,MAAMC,EAAWR,EAAUO,GAO3B,OANAD,EAAIC,GAAOE,SAAUC,KACnB,MAAMC,QAAeH,KAAYE,GAGjC,OADAf,EAAYiB,GAAaA,EAAUC,MAAM,GAAI,IACtCF,GAEFL,GACN,CAA0B,GAE7BX,EAAYmB,GAAa,IACpBA,EACH,CACEf,SACAC,UAAWE,EACXD,QACAc,MAAO,CAAA,MAGV,IAEGC,EAAclB,EAAamB,IAC/BtB,EAAYmB,IACV,MAAMI,EAAe,IAAIJ,GAOzB,OANII,EAAaC,OAAS,IACxBD,EAAaA,EAAaC,OAAS,GAAEC,EAChCF,GAAAA,EAAaA,EAAaC,OAAS,GACtCJ,CAAAA,MAAKK,EAAA,CAAA,EAAOH,MAGTC,GACR,EACA,IASH,OAAOG,EAAC/B,EAAagC,SAAQ,CAACC,MAPoB,CAChD7B,QACAG,cACAF,cACAqB,eAGoDvB,SAAGA,GAAQ,EAQnD,SAAA+B,IACd,MAAMC,EAAUC,EAAWpC,GAC3B,IAAKmC,EACH,UAAUE,MAAM,8DAElB,OAAOF,CACT,UAMgBG,IACd,MAAM/B,YAAEA,GAAgB2B,IACxB,OAAO3B,CACT,UAMgBgC,IACd,MAAMnC,MAAEA,EAAKsB,YAAEA,GAAgBQ,IAC/B,OAAqB,IAAjB9B,EAAMyB,OACD,CAAC,KAAMH,GAGT,CADOtB,EAAMA,EAAMyB,OAAS,GAAGJ,MACtBC,EAClB,CC5FM,MAAAc,EAAsCA,EAC1CC,gBACAC,eACAjC,OAAQkC,MAGR,MAAMvC,MAAEA,GAAU8B,IACZU,EAAkDC,EACtD,IAAMzC,EAAMyB,OAAS,EAAIzB,EAAMA,EAAMyB,OAAS,GAAK,KACnD,CAAEzB,IAGE0C,EAAmBC,EAA0C,MAE7DC,EAAkBxC,EAAayC,IAC/BA,EAAGC,gBAAkBD,EAAGE,QACZ,MAAZP,GAAAA,EAAclC,UAAUsC,iBAC1BJ,EAAalC,UAAUsC,gBAAgBC,EACzC,EACC,CAAEL,IAELQ,EAAU,KACR,MAAMC,EAAgBP,EAAiBQ,SAGlCD,GAAiBT,EACP,MAAbH,GAAAA,IAGOY,IAAkBT,IACb,MAAZF,GAAAA,KAIFI,EAAiBQ,QAAUV,CAAAA,EAC1B,CAAEA,EAAcH,EAAeC,IAElC,MAAMa,EAA2B,MAAZX,OAAY,EAAZA,EAAcnC,OAEnC,OACEsB,EAAAyB,EAAA,CAAArD,SACGoD,GACCxB,EAACY,EAAQ,CAACK,gBAAiBA,EAAe7C,SACxC4B,EAACwB,EAAYzB,EAAA,CAAA,EACPc,EAAalC,UACbkC,EAAajC,WAGlB,MAAI"}
|
package/dist/index.umd.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("react/jsx-runtime"),require("react")):"function"==typeof define&&define.amd?define(["exports","react/jsx-runtime","react"],n):n((e||self).reactModalPort={},e.jsx,e.react)}(this,function(e,n,t){function r(){return r=Object.assign?Object.assign.bind():function(e){for(var n=1;n<arguments.length;n++){var t=arguments[n];for(var r in t)({}).hasOwnProperty.call(t,r)&&(e[r]=t[r])}return e},r.apply(null,arguments)}var o=t.createContext(null);function u(){var e=t.useContext(o);if(!e)throw new Error("useModalContext must be used within a ModalContextProvider");return e}e.ModalContextProvider=function(e){var u=e.children,a=t.useState([]),l=a[0],c=a[1],i=t.useCallback(function(e,n,t){void 0===t&&(t={});var r=Object.keys(n).reduce(function(e,t){var r=n[t];return e[t]=function(){try{return Promise.resolve(r.apply(void 0,[].slice.call(arguments))).then(function(e){return c(function(e){return e.slice(0,-1)}),e})}catch(e){return Promise.reject(e)}},e},{});c(function(n){return[].concat(n,[{render:e,resolvers:r,props:t,state:{}}])})},[]),s=t.useCallback(function(e){c(function(n){var t=[].concat(n);return t.length>0&&(t[t.length-1]=r({},t[t.length-1],{state:r({},e)})),t})},[]);return n.jsx(o.Provider,{value:{stack:l,launchModal:i,updateStack:c,updateState:s},children:u})},e.ModalPort=function(e){var o=e.onModalLaunch,a=e.onModalClose,l=e.render,c=u().stack,i=t.useMemo(function(){return c.length>0?c[c.length-1]:null},[c]),s=t.useRef(null),d=t.useCallback(function(e){e.currentTarget===e.target&&null!=i&&i.resolvers.onBackdropClick&&i.resolvers.onBackdropClick(e)},[i]);t.useEffect(function(){var e=s.current;!e&&i?null==o||o():e&&!i&&(null==a||a()),s.current=i},[i,o,a]);var f=null==i?void 0:i.render;return n.jsx(n.Fragment,{children:f&&n.jsx(l,{onBackdropClick:d,children:n.jsx(f,r({},i.resolvers,i.props))})||null})},e.useModal=function(){return u().launchModal},e.useModalContext=u,e.useModalState=function(){var e=u(),n=e.stack,t=e.updateState;return 0===n.length?[null,t]:[n[n.length-1].state,t]}});
|
|
2
|
-
//# sourceMappingURL=index.umd.js.map
|