react-modal-port 0.3.0 → 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/readme.md CHANGED
@@ -1,313 +1,324 @@
1
1
  # react-modal-port
2
2
 
3
- This small but neat package allows you to:
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
- **Demo:** https://codepen.io/oliverwehn/pen/YzMyoBr?editors=0010
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
- ## 🏁 Installation
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
- $ npm install react-modal-port
30
+ npm install react-modal-port
24
31
  ```
25
32
 
26
- ## 🧩 Components
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
- ### ModalContextProvider
37
+ ```tsx
38
+ import type { ReactNode } from 'react';
39
+ import { ModalPort, ModalProvider, type ModalPortRenderProps } from 'react-modal-port';
29
40
 
30
- The `ModalContextProvider` wraps your app to allow your app’s components to launch or render modals.
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
- ### ModalPort
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 `ModalPort` defines the outlet in your app through which your modals will be rendered. It requires one prop, `render`, that expects a `React.FunctionComponent`. Any modal you’ll launch will be rendered as the child of the component passed via the prop. Typically, this component will provide a backdrop the modal is rendered on top.
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
- Besides `children`, a prop `onBackdropClick` is passed to `render`. It holds, if provided to the modal on launch, the click handler function that should be called if the backdrop is clicked, e.g. to dismiss the modal.
72
+ ## Launching modals
37
73
 
38
- The `ModalPort` accepts two addtional optional props `onModalLauch` and `onModalClose` that both expect a function. The provided functions will be called in the event of a modal being launched or, respectively, a modal being closed.
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
- import { ModalContextProvider } from 'react-modal-port';
43
-
44
- // Backdrop component to be passed to the `ModalPort`’s `render` prop
45
- const Backdrop: React.FC<ModalPortRenderProps> = ({ children, onBackdropClick }) => (
46
- <div
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 default RootLayout;
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
- ## 🚀 Launching Modals
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 './modals/decision';
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 [ decision, setDecision ] = useState<boolean|null>(null);
103
+ const [decision, setDecision] = useState<boolean | null>(null);
94
104
 
95
- const onClick = () => {
105
+ const ask = () => {
96
106
  launchModal(
97
- // Modal component
98
- DecisionModal,
99
- // Modal resolvers
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
- <div style="padding: 4rem;">
116
- <h1>Welcome</h1>
117
- {decision === null && (
118
- <p>Make your decision!</p>
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
- Modals can be created and designed to fully fit your needs. The only requirement is to define the functions to resolve them, for example, when clicking a button. The modal component for the example above could look like this:
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
- type DecisionModalProps = {
143
- decideYay: () => void;
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
- const DecisionModal: React.FC<DecisionModalProps> = ({
149
- decideYay,
150
- decideNay,
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
- export default DecisionModal;
159
+ ```tsx
160
+ launchModal(DecisionModal, resolvers, { question: 'Ship it?' }, { onDismiss: () => setDecision(null) });
171
161
  ```
172
162
 
173
- ## 🔂 Asynchronous Resolution
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
- Modals can be simple like the one above or highly complex like a full wizard experience or questionnaire. It is also possible to trigger asynchronous operations within a resolver function. As long as the resolver function returns a promise, the modal won’t close until the promise is resolved.
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
- const onClick = () => {
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
- // Modal component
184
- DecisionModal,
185
- // Modal resolvers
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
- It then could cater the following modal functionality:
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
- type DecisionModalProps = {
204
- decideYay: () => void;
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
- const DecisionModal: React.FC<DecisionModalProps> = ({
211
- decideYay,
212
- decideNay,
213
- letMeThinkAboutIt,
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
- <div
229
- style="background-color: white; border-radius: 0.5rem; padding: 1rem; display: flex; flex-direction: column; align-items: center; gap: 0.5rem;"
230
- role="dialog"
231
- aria-modal="true"
232
- aria-labeledby="modal-title"
233
- aria-describedby="modal-description"
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
- <h4 id="modal-title">How do you decide?</h4>
236
- <div id ="modal-description">
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
- export default DecisionModal;
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
- ## 🥞 Stacking Modals And Handling Modal State
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
- If you need to launch a modal from a modal, the modals become (logically) stacked. That means that the `ModalPort` will always render the modal at the top of the modal stack and will return to the previous one as soon as the current one is resolved. Via the resolver functions, data can be passed from the resolving modal to the modal it was launched from.
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
- In case you need to handle state within multiple stacked modal, you can create, access, and update the modal state using the `useModalState` hook. It returns a tuple with the current modal state and a function to update the state. The modal state is always an object holding key-value pairs. Each modal’s modal state will persist until the modal itself is resolved.
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
- In the example below, we’ll ask the user to enter their name and prompt them in a second modal to confirm. We’ll fall back to the first modal with the name previously entered if the user wishes to edit it once more.
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 React from "react";
261
- import ConfirmModal from "./ConfirmModal";
262
+ import { useModal, type LaunchModal } from 'react-modal-port';
262
263
 
263
- type AskForNameModalProps = {
264
- provideName: ModalResolver,
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
- export const AskForNameModal: React.FC<AskForNameModalProps> = ({
268
- provideName,
269
- }) => {
267
+ type AnyFn = (...args: never[]) => unknown;
270
268
 
269
+ export function useAnimatedModal(): LaunchModal {
271
270
  const launchModal = useModal();
272
- const [ modalState, updateModalState ] = useModalState();
273
- const onNameChange = (ev) => {
274
- updateModalState({
275
- ...modalState,
276
- name: ev.target.value,
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
- const onButtonClick = () => {
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
- return (
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
@@ -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"}
@@ -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