react-modal-registry 0.1.0 → 0.1.2
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 +120 -95
- package/dist/index.cjs +1 -320
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -92
- package/dist/index.d.ts +50 -92
- package/dist/index.js +1 -301
- package/dist/index.js.map +1 -1
- package/package.json +29 -16
package/README.md
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
# react-modal-registry
|
|
2
2
|
|
|
3
|
-
> Type-safe, promise-based modal
|
|
3
|
+
> Type-safe, promise-based modal and dialog management for React.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/react-modal-registry)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
[](https://bundlephobia.com/package/react-modal-registry)
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
9
9
|
|
|
10
|
+
Managing dialogs in React often leads to **modal sprawl**—scattering `useState(false)` flags, prop drilling, and duplicate dialog components across pages.
|
|
11
|
+
|
|
12
|
+
`react-modal-registry` provides a single, type-safe registry to open and manage modals cleanly:
|
|
13
|
+
|
|
14
|
+
- **Promise-Based Dialogs**: Open modals imperatively (`await modal.open()`) and await user responses inline without local boolean states.
|
|
15
|
+
- **End-to-End Type Safety**: Autocomplete modal IDs, input payloads, and return values via TypeScript declaration merging.
|
|
16
|
+
- **URL Deep-Linking**: Synchronize modal visibility with the URL hash (`#modal=<id>`) or query params with native browser Back button dismissal.
|
|
17
|
+
- **Automatic Focus Restoration**: Tracks and restores focus back to the trigger element on the page upon modal dismissal, following WAI-ARIA accessibility standards.
|
|
18
|
+
- **Zero Re-Render Penalty**: Split-context architecture ensures caller components never re-render when modals open or close.
|
|
19
|
+
- **Headless & UI Agnostic**: Works out of the box with shadcn/ui, Radix UI, Tailwind CSS, or native HTML `<dialog>`.
|
|
20
|
+
|
|
10
21
|
---
|
|
11
22
|
|
|
12
23
|
## Installation
|
|
@@ -27,36 +38,34 @@ bun add react-modal-registry
|
|
|
27
38
|
|
|
28
39
|
### 1. Mount `ModalProvider`
|
|
29
40
|
|
|
30
|
-
Wrap your application root
|
|
41
|
+
Wrap your application root:
|
|
31
42
|
|
|
32
43
|
```tsx
|
|
33
44
|
import { ModalProvider } from 'react-modal-registry'
|
|
34
45
|
|
|
35
|
-
export function
|
|
46
|
+
export function App({ children }: { children: React.ReactNode }) {
|
|
36
47
|
return <ModalProvider>{children}</ModalProvider>
|
|
37
48
|
}
|
|
38
49
|
```
|
|
39
50
|
|
|
40
51
|
### 2. Declare Modal Types
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
Declare modal IDs, payloads, and return types via TypeScript module augmentation (e.g., `env.d.ts` or `modals.d.ts`):
|
|
43
54
|
|
|
44
55
|
```ts
|
|
45
56
|
declare module 'react-modal-registry' {
|
|
46
|
-
interface
|
|
47
|
-
confirmDelete: {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
interface ModalResultMap {
|
|
52
|
-
confirmDelete: boolean
|
|
57
|
+
interface ModalRegistry {
|
|
58
|
+
confirmDelete: {
|
|
59
|
+
data: { name: string }
|
|
60
|
+
result: boolean
|
|
61
|
+
}
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
```
|
|
56
65
|
|
|
57
66
|
### 3. Create a Modal Component
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
Use `ModalProps<TData, TResult>` for your component props:
|
|
60
69
|
|
|
61
70
|
```tsx
|
|
62
71
|
import type { ModalProps } from 'react-modal-registry'
|
|
@@ -65,160 +74,176 @@ export function ConfirmDeleteModal({
|
|
|
65
74
|
isOpen,
|
|
66
75
|
data,
|
|
67
76
|
close
|
|
68
|
-
}: ModalProps<{
|
|
77
|
+
}: ModalProps<{ name: string }, boolean>) {
|
|
69
78
|
if (!isOpen) return null
|
|
70
79
|
|
|
71
80
|
return (
|
|
72
81
|
<dialog open={isOpen}>
|
|
73
|
-
<
|
|
74
|
-
<button onClick={() => close(true)}>
|
|
82
|
+
<p>Are you sure you want to delete {data.name}?</p>
|
|
83
|
+
<button onClick={() => close(true)}>Delete</button>
|
|
75
84
|
<button onClick={() => close(false)}>Cancel</button>
|
|
76
85
|
</dialog>
|
|
77
86
|
)
|
|
78
87
|
}
|
|
79
88
|
```
|
|
80
89
|
|
|
81
|
-
### 4. Register
|
|
90
|
+
### 4. Register and Open
|
|
82
91
|
|
|
83
|
-
|
|
92
|
+
Register modals at app root or feature level, and trigger them anywhere with `useModal`:
|
|
84
93
|
|
|
85
94
|
```tsx
|
|
86
|
-
import { ModalRegistry } from 'react-modal-registry'
|
|
95
|
+
import { ModalRegistry, useModal } from 'react-modal-registry'
|
|
87
96
|
import { ConfirmDeleteModal } from './ConfirmDeleteModal'
|
|
88
97
|
|
|
89
|
-
const
|
|
98
|
+
const modals = [{ id: 'confirmDelete', component: ConfirmDeleteModal }] as const
|
|
90
99
|
|
|
91
|
-
export function
|
|
100
|
+
export function Layout({ children }: { children: React.ReactNode }) {
|
|
92
101
|
return (
|
|
93
102
|
<>
|
|
94
|
-
<ModalRegistry modals={
|
|
103
|
+
<ModalRegistry modals={modals} />
|
|
95
104
|
{children}
|
|
96
105
|
</>
|
|
97
106
|
)
|
|
98
107
|
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### 5. Trigger Anywhere with `useModal`
|
|
102
|
-
|
|
103
|
-
```tsx
|
|
104
|
-
import { useModal } from 'react-modal-registry'
|
|
105
108
|
|
|
106
|
-
export function DeleteButton({
|
|
109
|
+
export function DeleteButton({ itemName }: { itemName: string }) {
|
|
107
110
|
const confirmModal = useModal('confirmDelete')
|
|
108
111
|
|
|
109
112
|
const handleDelete = async () => {
|
|
110
|
-
|
|
111
|
-
const confirmed = await confirmModal.open({
|
|
112
|
-
userId: user.id,
|
|
113
|
-
name: user.name
|
|
114
|
-
})
|
|
115
|
-
|
|
113
|
+
const confirmed = await confirmModal.open({ name: itemName })
|
|
116
114
|
if (confirmed) {
|
|
117
|
-
|
|
115
|
+
// item deleted
|
|
118
116
|
}
|
|
119
117
|
}
|
|
120
118
|
|
|
121
|
-
return <button onClick={handleDelete}>Delete
|
|
119
|
+
return <button onClick={handleDelete}>Delete</button>
|
|
122
120
|
}
|
|
123
121
|
```
|
|
124
122
|
|
|
125
123
|
---
|
|
126
124
|
|
|
127
|
-
## Features
|
|
125
|
+
## Usage & Features
|
|
128
126
|
|
|
129
|
-
|
|
130
|
-
- **Promise-Based & Event-Driven**: `await modal.open(data)` for async results or pass callbacks (`onScan`, `onSuccess`) directly in payloads.
|
|
131
|
-
- **Zero Re-Render Overhead**: Split-context architecture guarantees callers of `useModal()` never re-render when modals open, close, or animate.
|
|
132
|
-
- **Code-Splitting Ready**: Register modals with `React.lazy()` at app shells without importing modal components into caller files.
|
|
133
|
-
- **Lightweight & Headless**: Zero external runtime dependencies (~1.3 kB min+gzip). Works with any styling solution (Tailwind, Radix, Shadcn, MUI).
|
|
127
|
+
### Promise-Based Responses
|
|
134
128
|
|
|
135
|
-
|
|
129
|
+
`modal.open(data)` returns a `Promise<TResult | undefined>`. Calling `close(result)` resolves the promise:
|
|
136
130
|
|
|
137
|
-
|
|
131
|
+
```tsx
|
|
132
|
+
const confirmed = await modal.open({ name: 'Project Alpha' })
|
|
133
|
+
if (confirmed) {
|
|
134
|
+
await api.deleteProject()
|
|
135
|
+
}
|
|
136
|
+
```
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
| :--------------------------- | :-----------------------------------------------------------: | :---------------------------: | :---------------------------: | :------------------------: |
|
|
141
|
-
| **Type Safety** | **Declaration merging** (autocompletes ID, payload, & return) | String ID / loose types | Direct component import | Direct component import |
|
|
142
|
-
| **Caller Re-renders** | **None** (isolated split contexts) | Re-renders on context updates | Re-renders with wrapper state | Re-renders on state toggle |
|
|
143
|
-
| **Promise Returns** | **Strictly typed** (`Promise<TResult>`) | Untyped (`Promise<unknown>`) | Manual generic | Not supported |
|
|
144
|
-
| **Code Splitting** | **Native** (`React.lazy()` at registration shell) | Requires HOC per modal | Bundled at caller | Bundled at caller |
|
|
145
|
-
| **Component Wrapper** | **None** (standard `ModalProps`) | Requires `NiceModal.create()` | Requires `create()` wrapper | Requires hook binding |
|
|
146
|
-
| **UI Agnostic** | Yes (Tailwind, Radix, Shadcn, MUI) | Yes | Yes | Tied to `react-modal` |
|
|
147
|
-
| **Bundle Size (min + gzip)** | **~1.3 kB** | ~2.4 kB | ~1.6 kB | ~1.3 kB |
|
|
138
|
+
If the modal is dismissed without a value (e.g. backdrop click or ESC), the promise resolves to `undefined`.
|
|
148
139
|
|
|
149
|
-
|
|
140
|
+
### Rich Payloads & Callbacks
|
|
150
141
|
|
|
151
|
-
|
|
142
|
+
Pay-load data is passed in-memory, so you can pass callbacks, functions, or complex objects directly:
|
|
152
143
|
|
|
153
|
-
|
|
144
|
+
```ts
|
|
145
|
+
declare module 'react-modal-registry' {
|
|
146
|
+
interface ModalRegistry {
|
|
147
|
+
scanner: {
|
|
148
|
+
data: { onScan: (code: string) => void }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
const scannerModal = useModal('scanner')
|
|
154
156
|
|
|
155
|
-
|
|
157
|
+
scannerModal.open({
|
|
158
|
+
onScan: code => {
|
|
159
|
+
console.log('Scanned:', code)
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Component-Scoped Registrations
|
|
165
|
+
|
|
166
|
+
Register modals that exist only while a specific component is mounted:
|
|
156
167
|
|
|
157
168
|
```tsx
|
|
158
|
-
import { useRegisterModal } from 'react-modal-registry'
|
|
159
|
-
import {
|
|
169
|
+
import { useRegisterModal, useModal } from 'react-modal-registry'
|
|
170
|
+
import { FeatureModal } from './FeatureModal'
|
|
160
171
|
|
|
161
|
-
export function
|
|
162
|
-
useRegisterModal('
|
|
172
|
+
export function FeaturePage() {
|
|
173
|
+
useRegisterModal('featureDetails', FeatureModal)
|
|
174
|
+
const modal = useModal('featureDetails')
|
|
163
175
|
|
|
164
|
-
return <
|
|
176
|
+
return <button onClick={() => modal.open()}>Open Details</button>
|
|
165
177
|
}
|
|
166
178
|
```
|
|
167
179
|
|
|
168
|
-
###
|
|
180
|
+
### Code Splitting with `React.lazy`
|
|
169
181
|
|
|
170
|
-
|
|
182
|
+
Split modal components into separate chunks:
|
|
171
183
|
|
|
172
184
|
```tsx
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
import { lazy } from 'react'
|
|
186
|
+
import { ModalRegistry } from 'react-modal-registry'
|
|
175
187
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
})
|
|
180
|
-
}
|
|
188
|
+
const AnalyticsModal = lazy(() => import('./AnalyticsModal'))
|
|
189
|
+
|
|
190
|
+
const modals = [{ id: 'analytics', component: AnalyticsModal }] as const
|
|
181
191
|
|
|
182
|
-
|
|
192
|
+
export function App() {
|
|
193
|
+
return <ModalRegistry modals={modals} />
|
|
183
194
|
}
|
|
184
195
|
```
|
|
185
196
|
|
|
186
|
-
###
|
|
197
|
+
### URL Deep Linking
|
|
187
198
|
|
|
188
|
-
|
|
199
|
+
Optionally sync active modals with the browser URL (hash or query) and browser Back button:
|
|
189
200
|
|
|
190
201
|
```tsx
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
<ModalProvider
|
|
203
|
+
routing={{
|
|
204
|
+
strategy: 'hash', // 'hash' (#modal=changelog) or 'query' (?modal=changelog)
|
|
205
|
+
param: 'modal', // default: 'modal'
|
|
206
|
+
historyMode: 'push', // 'push' or 'replace'
|
|
207
|
+
defaultData: {
|
|
208
|
+
changelog: { version: '1.0.0' }
|
|
209
|
+
}
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
{children}
|
|
213
|
+
</ModalProvider>
|
|
197
214
|
```
|
|
198
215
|
|
|
216
|
+
When routing is enabled:
|
|
217
|
+
- Opening a modal syncs `#modal=<id>` to the URL.
|
|
218
|
+
- Closing the modal cleans up the URL parameter.
|
|
219
|
+
- Pressing the browser **Back** button dismisses the modal.
|
|
220
|
+
|
|
199
221
|
---
|
|
200
222
|
|
|
201
223
|
## API Reference
|
|
202
224
|
|
|
203
|
-
| Export
|
|
204
|
-
|
|
|
205
|
-
| `<ModalProvider>`
|
|
206
|
-
| `<ModalRegistry>`
|
|
207
|
-
| `useModal(id)`
|
|
208
|
-
| `useIsModalOpen(id)`
|
|
209
|
-
| `useRegisterModal(id, component)` | Hook
|
|
210
|
-
| `useCloseAll()`
|
|
211
|
-
| `ModalProps<TData, TResult>`
|
|
212
|
-
| `
|
|
213
|
-
| `ModalResultMap` | Interface | Augmentation interface for return types. |
|
|
214
|
-
| `ModalRegistry` | Interface | Unified augmentation interface for paired data and result types. |
|
|
225
|
+
| Export | Type | Description |
|
|
226
|
+
| :--- | :--- | :--- |
|
|
227
|
+
| `<ModalProvider>` | Component | Root provider managing modal state and slot rendering. Accepts `unmountDelay`, `fallback`, and `routing`. |
|
|
228
|
+
| `<ModalRegistry>` | Component | Mounts a batch of static modal registrations: `<ModalRegistry modals={[...]} />`. |
|
|
229
|
+
| `useModal(id)` | Hook | Returns imperative controls: `{ open(data?), close(result?), isOpen() }`. |
|
|
230
|
+
| `useIsModalOpen(id)` | Hook | Reactively subscribes to whether a modal is currently open (`boolean`). |
|
|
231
|
+
| `useRegisterModal(id, component)` | Hook | Dynamically registers a modal for the calling component's lifecycle. |
|
|
232
|
+
| `useCloseAll()` | Hook | Dismisses all currently open modals. |
|
|
233
|
+
| `ModalProps<TData, TResult>` | Interface | Props received by modal components: `{ isOpen, data, close }`. |
|
|
234
|
+
| `ModalRegistry` | Interface | Target interface for module augmentation. |
|
|
215
235
|
|
|
216
236
|
---
|
|
217
237
|
|
|
218
|
-
##
|
|
238
|
+
## Interactive Demo
|
|
239
|
+
|
|
240
|
+
A runnable demo showcasing forms, wizards, drawers, and deep linking is available in the [`demo`](./demo) directory:
|
|
219
241
|
|
|
220
|
-
|
|
221
|
-
|
|
242
|
+
```bash
|
|
243
|
+
cd demo
|
|
244
|
+
pnpm install
|
|
245
|
+
pnpm dev
|
|
246
|
+
```
|
|
222
247
|
|
|
223
248
|
---
|
|
224
249
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,321 +1,2 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
"use client";
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
-
};
|
|
11
|
-
var __copyProps = (to, from, except, desc) => {
|
|
12
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
-
for (let key of __getOwnPropNames(from))
|
|
14
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
-
|
|
21
|
-
// src/index.tsx
|
|
22
|
-
var index_exports = {};
|
|
23
|
-
__export(index_exports, {
|
|
24
|
-
ModalProvider: () => ModalProvider,
|
|
25
|
-
ModalRegistry: () => ModalRegistry,
|
|
26
|
-
useCloseAll: () => useCloseAll,
|
|
27
|
-
useIsModalOpen: () => useIsModalOpen,
|
|
28
|
-
useModal: () => useModal,
|
|
29
|
-
useRegisterModal: () => useRegisterModal
|
|
30
|
-
});
|
|
31
|
-
module.exports = __toCommonJS(index_exports);
|
|
32
|
-
var import_react = require("react");
|
|
33
|
-
var import_jsx_runtime = require("react/jsx-runtime");
|
|
34
|
-
var ModalActionsContext = (0, import_react.createContext)(null);
|
|
35
|
-
var ModalSlotsContext = (0, import_react.createContext)(null);
|
|
36
|
-
var UNMOUNT_DELAY_MS = 200;
|
|
37
|
-
function ModalProvider({ children }) {
|
|
38
|
-
const slotsRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
39
|
-
const pendingRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
40
|
-
const timersRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
41
|
-
const [slots, setSlots] = (0, import_react.useState)(/* @__PURE__ */ new Map());
|
|
42
|
-
(0, import_react.useEffect)(() => {
|
|
43
|
-
const timers = timersRef.current;
|
|
44
|
-
const pendings = pendingRef.current;
|
|
45
|
-
return () => {
|
|
46
|
-
for (const timer of timers.values()) {
|
|
47
|
-
clearTimeout(timer);
|
|
48
|
-
}
|
|
49
|
-
timers.clear();
|
|
50
|
-
for (const pending of pendings.values()) {
|
|
51
|
-
pending.resolve(void 0);
|
|
52
|
-
}
|
|
53
|
-
pendings.clear();
|
|
54
|
-
};
|
|
55
|
-
}, []);
|
|
56
|
-
const sync = (0, import_react.useCallback)(() => {
|
|
57
|
-
setSlots(new Map(slotsRef.current));
|
|
58
|
-
}, []);
|
|
59
|
-
const register = (0, import_react.useCallback)(
|
|
60
|
-
(idOrModals, component) => {
|
|
61
|
-
const items = Array.isArray(idOrModals) ? idOrModals : component ? [
|
|
62
|
-
{
|
|
63
|
-
id: idOrModals,
|
|
64
|
-
component
|
|
65
|
-
}
|
|
66
|
-
] : [];
|
|
67
|
-
let changed = false;
|
|
68
|
-
for (const { id, component: comp } of items) {
|
|
69
|
-
const existing = slotsRef.current.get(id);
|
|
70
|
-
const typedComp = comp;
|
|
71
|
-
if (existing) {
|
|
72
|
-
if (existing.component !== typedComp) {
|
|
73
|
-
slotsRef.current.set(id, {
|
|
74
|
-
...existing,
|
|
75
|
-
component: typedComp
|
|
76
|
-
});
|
|
77
|
-
changed = true;
|
|
78
|
-
}
|
|
79
|
-
} else {
|
|
80
|
-
slotsRef.current.set(id, {
|
|
81
|
-
id,
|
|
82
|
-
component: typedComp,
|
|
83
|
-
isOpen: false,
|
|
84
|
-
isMounted: false,
|
|
85
|
-
data: void 0
|
|
86
|
-
});
|
|
87
|
-
changed = true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (changed) {
|
|
91
|
-
sync();
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
[sync]
|
|
95
|
-
);
|
|
96
|
-
const unregister = (0, import_react.useCallback)(
|
|
97
|
-
(idOrIds) => {
|
|
98
|
-
const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
|
99
|
-
let changed = false;
|
|
100
|
-
for (const id of ids) {
|
|
101
|
-
const timer = timersRef.current.get(id);
|
|
102
|
-
if (timer) {
|
|
103
|
-
clearTimeout(timer);
|
|
104
|
-
timersRef.current.delete(id);
|
|
105
|
-
}
|
|
106
|
-
const pending = pendingRef.current.get(id);
|
|
107
|
-
if (pending) {
|
|
108
|
-
pendingRef.current.delete(id);
|
|
109
|
-
pending.resolve(void 0);
|
|
110
|
-
}
|
|
111
|
-
if (slotsRef.current.has(id)) {
|
|
112
|
-
slotsRef.current.delete(id);
|
|
113
|
-
changed = true;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (changed) {
|
|
117
|
-
sync();
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
[sync]
|
|
121
|
-
);
|
|
122
|
-
const open = (0, import_react.useCallback)(
|
|
123
|
-
(id, ...args) => {
|
|
124
|
-
const data = args[0];
|
|
125
|
-
const slot = slotsRef.current.get(id);
|
|
126
|
-
if (!slot) {
|
|
127
|
-
return Promise.reject(
|
|
128
|
-
new Error(`[react-modal-registry] Modal "${String(id)}" is not registered.`)
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
const existing = pendingRef.current.get(id);
|
|
132
|
-
if (slot.isOpen && slot.data === data && existing) {
|
|
133
|
-
return existing.promise;
|
|
134
|
-
}
|
|
135
|
-
if (existing) {
|
|
136
|
-
pendingRef.current.delete(id);
|
|
137
|
-
existing.resolve(void 0);
|
|
138
|
-
}
|
|
139
|
-
const timer = timersRef.current.get(id);
|
|
140
|
-
if (timer) {
|
|
141
|
-
clearTimeout(timer);
|
|
142
|
-
timersRef.current.delete(id);
|
|
143
|
-
}
|
|
144
|
-
let resolver;
|
|
145
|
-
const promise = new Promise((resolve) => {
|
|
146
|
-
resolver = resolve;
|
|
147
|
-
});
|
|
148
|
-
pendingRef.current.set(id, {
|
|
149
|
-
promise,
|
|
150
|
-
resolve: resolver
|
|
151
|
-
});
|
|
152
|
-
slotsRef.current.set(id, { ...slot, isOpen: true, isMounted: true, data });
|
|
153
|
-
sync();
|
|
154
|
-
return promise;
|
|
155
|
-
},
|
|
156
|
-
[sync]
|
|
157
|
-
);
|
|
158
|
-
const scheduleUnmount = (0, import_react.useCallback)(
|
|
159
|
-
(id) => {
|
|
160
|
-
const timer = timersRef.current.get(id);
|
|
161
|
-
if (timer) clearTimeout(timer);
|
|
162
|
-
timersRef.current.set(
|
|
163
|
-
id,
|
|
164
|
-
setTimeout(() => {
|
|
165
|
-
timersRef.current.delete(id);
|
|
166
|
-
const current = slotsRef.current.get(id);
|
|
167
|
-
if (current && !current.isOpen) {
|
|
168
|
-
slotsRef.current.set(id, { ...current, isMounted: false });
|
|
169
|
-
sync();
|
|
170
|
-
}
|
|
171
|
-
}, UNMOUNT_DELAY_MS)
|
|
172
|
-
);
|
|
173
|
-
},
|
|
174
|
-
[sync]
|
|
175
|
-
);
|
|
176
|
-
const close = (0, import_react.useCallback)(
|
|
177
|
-
(id, result) => {
|
|
178
|
-
const slot = slotsRef.current.get(id);
|
|
179
|
-
if (!slot || !slot.isOpen) return;
|
|
180
|
-
const pending = pendingRef.current.get(id);
|
|
181
|
-
if (pending) {
|
|
182
|
-
pendingRef.current.delete(id);
|
|
183
|
-
pending.resolve(result);
|
|
184
|
-
}
|
|
185
|
-
slotsRef.current.set(id, { ...slot, isOpen: false, isMounted: true });
|
|
186
|
-
sync();
|
|
187
|
-
scheduleUnmount(id);
|
|
188
|
-
},
|
|
189
|
-
[sync, scheduleUnmount]
|
|
190
|
-
);
|
|
191
|
-
const closeAll = (0, import_react.useCallback)(() => {
|
|
192
|
-
for (const [, pending] of pendingRef.current) {
|
|
193
|
-
pending.resolve(void 0);
|
|
194
|
-
}
|
|
195
|
-
pendingRef.current.clear();
|
|
196
|
-
let changed = false;
|
|
197
|
-
for (const [id, slot] of slotsRef.current) {
|
|
198
|
-
if (slot.isOpen) {
|
|
199
|
-
slotsRef.current.set(id, { ...slot, isOpen: false, isMounted: true });
|
|
200
|
-
scheduleUnmount(id);
|
|
201
|
-
changed = true;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (changed) {
|
|
205
|
-
sync();
|
|
206
|
-
}
|
|
207
|
-
}, [sync, scheduleUnmount]);
|
|
208
|
-
const isOpen = (0, import_react.useCallback)((id) => slotsRef.current.get(id)?.isOpen ?? false, []);
|
|
209
|
-
const actions = (0, import_react.useMemo)(
|
|
210
|
-
() => ({
|
|
211
|
-
register,
|
|
212
|
-
unregister,
|
|
213
|
-
open,
|
|
214
|
-
close,
|
|
215
|
-
closeAll,
|
|
216
|
-
isOpen
|
|
217
|
-
}),
|
|
218
|
-
[register, unregister, open, close, closeAll, isOpen]
|
|
219
|
-
);
|
|
220
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalActionsContext.Provider, { value: actions, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ModalSlotsContext.Provider, { value: slots, children: [
|
|
221
|
-
children,
|
|
222
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalRenderer, {})
|
|
223
|
-
] }) });
|
|
224
|
-
}
|
|
225
|
-
function useRegisterModal(idOrModals, component) {
|
|
226
|
-
const { register, unregister } = useModalContext();
|
|
227
|
-
(0, import_react.useEffect)(() => {
|
|
228
|
-
if (Array.isArray(idOrModals)) {
|
|
229
|
-
register(idOrModals);
|
|
230
|
-
return () => {
|
|
231
|
-
unregister(idOrModals.map((m) => m.id));
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
if (component) {
|
|
235
|
-
register(idOrModals, component);
|
|
236
|
-
return () => {
|
|
237
|
-
unregister(idOrModals);
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
return void 0;
|
|
241
|
-
}, [idOrModals, component, register, unregister]);
|
|
242
|
-
}
|
|
243
|
-
function ModalRegistry({
|
|
244
|
-
modals
|
|
245
|
-
}) {
|
|
246
|
-
useRegisterModal(modals);
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
function useModal(id) {
|
|
250
|
-
const { open, close, isOpen } = useModalContext();
|
|
251
|
-
return (0, import_react.useMemo)(
|
|
252
|
-
() => ({
|
|
253
|
-
open: (...args) => open(id, ...args),
|
|
254
|
-
close: (result) => close(id, result),
|
|
255
|
-
isOpen: () => isOpen(id)
|
|
256
|
-
}),
|
|
257
|
-
[open, close, isOpen, id]
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
function useIsModalOpen(id) {
|
|
261
|
-
const slots = (0, import_react.useContext)(ModalSlotsContext);
|
|
262
|
-
if (!slots) {
|
|
263
|
-
throw new Error("useIsModalOpen must be used within <ModalProvider>");
|
|
264
|
-
}
|
|
265
|
-
return slots.get(id)?.isOpen ?? false;
|
|
266
|
-
}
|
|
267
|
-
function useCloseAll() {
|
|
268
|
-
return useModalContext().closeAll;
|
|
269
|
-
}
|
|
270
|
-
var ModalSlotItem = (0, import_react.memo)(function ModalSlotItem2({
|
|
271
|
-
id,
|
|
272
|
-
component: Component,
|
|
273
|
-
isOpen,
|
|
274
|
-
data,
|
|
275
|
-
close
|
|
276
|
-
}) {
|
|
277
|
-
const handleClose = (0, import_react.useCallback)(
|
|
278
|
-
(result) => {
|
|
279
|
-
close(id, result);
|
|
280
|
-
},
|
|
281
|
-
[close, id]
|
|
282
|
-
);
|
|
283
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, { isOpen, data, close: handleClose }) });
|
|
284
|
-
});
|
|
285
|
-
function ModalRenderer() {
|
|
286
|
-
const slots = (0, import_react.useContext)(ModalSlotsContext);
|
|
287
|
-
const { close } = useModalContext();
|
|
288
|
-
if (!slots) {
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: Array.from(slots.values()).map(
|
|
292
|
-
(slot) => slot.isMounted ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
293
|
-
ModalSlotItem,
|
|
294
|
-
{
|
|
295
|
-
id: slot.id,
|
|
296
|
-
component: slot.component,
|
|
297
|
-
isOpen: slot.isOpen,
|
|
298
|
-
data: slot.data,
|
|
299
|
-
close
|
|
300
|
-
},
|
|
301
|
-
slot.id
|
|
302
|
-
) : null
|
|
303
|
-
) });
|
|
304
|
-
}
|
|
305
|
-
function useModalContext() {
|
|
306
|
-
const context = (0, import_react.useContext)(ModalActionsContext);
|
|
307
|
-
if (!context) {
|
|
308
|
-
throw new Error("useModalContext must be used within <ModalProvider>");
|
|
309
|
-
}
|
|
310
|
-
return context;
|
|
311
|
-
}
|
|
312
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
313
|
-
0 && (module.exports = {
|
|
314
|
-
ModalProvider,
|
|
315
|
-
ModalRegistry,
|
|
316
|
-
useCloseAll,
|
|
317
|
-
useIsModalOpen,
|
|
318
|
-
useModal,
|
|
319
|
-
useRegisterModal
|
|
320
|
-
});
|
|
1
|
+
"use strict";"use client";var E=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var X=Object.getOwnPropertyNames;var Z=Object.prototype.hasOwnProperty;var ee=(t,e)=>{for(var s in e)E(t,s,{get:e[s],enumerable:!0})},te=(t,e,s,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let l of X(e))!Z.call(t,l)&&l!==s&&E(t,l,{get:()=>e[l],enumerable:!(a=W(e,l))||a.enumerable});return t};var ne=t=>te(E({},"__esModule",{value:!0}),t);var fe={};ee(fe,{ModalProvider:()=>se,ModalRegistry:()=>ue,isBrowser:()=>D,useCloseAll:()=>Me,useIsModalOpen:()=>pe,useModal:()=>ce,useRegisterModal:()=>G});module.exports=ne(fe);var r=require("react"),R=require("react/jsx-runtime"),k={strategy:"hash",param:"modal",historyMode:"push"},D=()=>typeof window<"u",oe=200,_=(0,r.createContext)(null),B=(0,r.createContext)(new Map);function re(t,e){if(Object.is(t,e))return!0;if(typeof t!="object"||t===null||typeof e!="object"||e===null)return!1;if(t instanceof Date&&e instanceof Date)return t.getTime()===e.getTime();if(t instanceof RegExp&&e instanceof RegExp)return t.source===e.source&&t.flags===e.flags;if(t instanceof Map||t instanceof Set||e instanceof Map||e instanceof Set)return!1;let s=Object.keys(t),a=Object.keys(e);return s.length!==a.length?!1:s.every(l=>Object.prototype.hasOwnProperty.call(e,l)&&t[l]===e[l])}function ae(t){let e=(0,r.useRef)(t);return Array.isArray(t)&&Array.isArray(e.current)&&e.current.length===t.length&&e.current.every((s,a)=>s.id===t[a].id&&s.component===t[a].component)?e.current:(e.current=t,t)}function z(t){let e=t.replace(/^#/,""),[s,a]=e.includes("?")?e.split("?",2):e.includes("=")?["",e]:[e,""];return{anchor:s,params:new URLSearchParams(a)}}function de(t,e){let s=e.toString();return s?`#${t?`${t}?`:""}${s}`:t?`#${t}`:""}function b(t){if(!D()||!t||t.enabled===!1)return null;let e=t.param??k.param;return(t.strategy??k.strategy)==="query"?new URLSearchParams(window.location.search).get(e):z(window.location.hash).params.get(e)}function O(t,e,s){if(!D()||!e||e.enabled===!1||b(e)===t)return;let a=s??e.historyMode??k.historyMode,l=e.param??k.param,M=new URL(window.location.href),x=f=>t?f.set(l,t):f.delete(l);if((e.strategy??k.strategy)==="query")x(M.searchParams);else{let{anchor:f,params:I}=z(window.location.hash);x(I),M.hash=de(f,I)}window.history[a==="push"?"pushState":"replaceState"](window.history.state,"",M.pathname+M.search+M.hash)}function K(t,e){if(t)return typeof t=="function"?t(e):Object.prototype.hasOwnProperty.call(t,e)?t[e]:void 0}function se({children:t,unmountDelay:e=oe,fallback:s=null,routing:a}){let l=(0,r.useRef)(new Map),[M,x]=(0,r.useState)(new Map),f=(0,r.useRef)(M),I=(0,r.useCallback)(n=>{let o=n(f.current);return f.current=o,x(o),o},[]),m=(0,r.useRef)(new Map),y=(0,r.useRef)(new Map),Y=(0,r.useRef)(0),C=(0,r.useMemo)(()=>a?{...k,...a,enabled:a.enabled!==!1}:null,[a?.enabled,a?.strategy,a?.param,a?.historyMode,a?.defaultData]),p=(0,r.useRef)(C);p.current=C,(0,r.useEffect)(()=>()=>{y.current.forEach(clearTimeout),y.current.clear(),m.current.forEach(n=>n.resolve(void 0)),m.current.clear()},[]);let P=(0,r.useCallback)(n=>{let o=l.current.get(n);return o?.[o.length-1]},[]),U=(0,r.useCallback)((n,o)=>{let c=y.current.get(n);c&&clearTimeout(c);let d;d=setTimeout(()=>{y.current.get(n)===d&&y.current.delete(n),I(i=>{let u=i.get(n);if(!u||u.isOpen||u.invocationId!==o)return i;let g=new Map(i);return g.delete(n),g})},e),y.current.set(n,d)},[e,I]),A=(0,r.useCallback)((n,o,c,d=!0)=>{let i=f.current.get(n);if(!i||!i.isOpen||o!==void 0&&i.invocationId!==o)return;let u=m.current.get(n);u&&(o===void 0||u.invocationId===o)&&(m.current.delete(n),u.resolve(c)),d&&p.current?.enabled&&b(p.current)===String(n)&&O(null,p.current,"replace"),i.prevFocus?.focus();let g=!1;I(T=>{let v=T.get(n);if(!v||!v.isOpen||o!==void 0&&v.invocationId!==o)return T;g=!0;let h=new Map(T);return e<=0?h.delete(n):h.set(n,{...v,isOpen:!1}),h}),g&&e>0&&U(n,i.invocationId)},[U,e,I]),w=(0,r.useCallback)((n,o,c=!0)=>{let d=P(n);if(!d)return Promise.reject(new Error(`[react-modal-registry] Modal "${String(n)}" is not registered.`));let i=f.current.get(n),u=m.current.get(n);if(i?.isOpen&&i.component===d&&re(i.data,o)&&u)return u.promise;c&&p.current?.enabled&&O(String(n),p.current),u&&(m.current.delete(n),u.resolve(void 0));let g=y.current.get(n);g&&(clearTimeout(g),y.current.delete(n));let T=++Y.current,v,h=new Promise(S=>{v=S});m.current.set(n,{promise:h,resolve:v,invocationId:T});let Q=i?.prevFocus??(D()&&document.activeElement instanceof HTMLElement&&document.activeElement!==document.body?document.activeElement:null);return I(S=>{let V=new Map(S);return V.set(n,{id:n,component:d,isOpen:!0,data:o,invocationId:T,prevFocus:Q}),V}),h},[P,I]),F=(0,r.useCallback)((n,...o)=>w(n,o[0],!0),[w]),N=(0,r.useCallback)((n,o)=>{A(n,void 0,o,!0)},[A]),j=(0,r.useCallback)(()=>{let n=Array.from(m.current.values());if(m.current.clear(),p.current?.enabled&&O(null,p.current,"replace"),e<=0){for(let o of y.current.values())clearTimeout(o);y.current.clear(),I(()=>new Map)}else{for(let[o,c]of f.current)c.isOpen&&U(o,c.invocationId);I(o=>{let c=!1,d=new Map(o);for(let[i,u]of o)u.isOpen&&(d.set(i,{...u,isOpen:!1}),c=!0);return c?d:o})}for(let o of f.current.values())if(o.isOpen&&o.prevFocus){o.prevFocus.focus();break}n.forEach(o=>o.resolve(void 0))},[U,e,I]),H=(0,r.useCallback)(n=>f.current.get(n)?.isOpen??!1,[]),$=(0,r.useCallback)(((n,o)=>{let c=Array.isArray(n)?n:o?[{id:n,component:o}]:[],d=p.current?.enabled?b(p.current):null;for(let i of c){let u=l.current.get(i.id);if(u||(u=[],l.current.set(i.id,u)),u.push(i.component),d&&d===String(i.id)&&!f.current.get(i.id)?.isOpen){let g=K(p.current?.defaultData,i.id);w(i.id,g,!1)}}}),[w]),q=(0,r.useCallback)((n,o)=>{let c=Array.isArray(n)?n:[n];for(let d of c){let i=l.current.get(d);if(!i)continue;if(o){let T=i.lastIndexOf(o);if(T!==-1&&i.splice(T,1),i.length>0)continue}l.current.delete(d);let u=y.current.get(d);u&&(clearTimeout(u),y.current.delete(d));let g=m.current.get(d);g&&(m.current.delete(d),g.resolve(void 0)),p.current?.enabled&&b(p.current)===String(d)&&O(null,p.current,"replace"),I(T=>{if(!T.has(d))return T;let v=new Map(T);return v.delete(d),v})}},[I]);(0,r.useEffect)(()=>{if(!C||!C.enabled||!D())return;let n=()=>{let o=b(p.current),c=f.current;for(let[d,i]of c)String(d)!==o&&i.isOpen&&A(d,void 0,void 0,!1);if(o){let d=o;!c.get(d)?.isOpen&&P(d)&&w(d,K(p.current?.defaultData,d),!1)}};return n(),window.addEventListener("popstate",n),window.addEventListener("hashchange",n),()=>{window.removeEventListener("popstate",n),window.removeEventListener("hashchange",n)}},[C,P,w,A]);let J=(0,r.useMemo)(()=>({register:$,unregister:q,open:F,close:N,closeAll:j,isOpen:H}),[$,q,F,N,j,H]);return(0,R.jsx)(_.Provider,{value:J,children:(0,R.jsxs)(B.Provider,{value:M,children:[t,(0,R.jsx)(le,{activeModals:M,close:A,fallback:s})]})})}function le({activeModals:t,close:e,fallback:s}){return t.size===0?null:(0,R.jsx)(R.Fragment,{children:Array.from(t.values(),a=>(0,R.jsx)(ie,{active:a,onClose:e,fallback:s},`${String(a.id)}:${a.invocationId}`))})}var ie=(0,r.memo)(function({active:e,onClose:s,fallback:a}){let l=(0,r.useCallback)(x=>s(e.id,e.invocationId,x,!0),[s,e.id,e.invocationId]),M=e.component;return(0,R.jsx)(r.Suspense,{fallback:a??null,children:(0,R.jsx)(M,{isOpen:e.isOpen,data:e.data,close:l})})});function G(t,e){let{register:s,unregister:a}=L(),l=ae(t);(0,r.useEffect)(()=>{if(Array.isArray(l))return s(l),()=>{for(let M of l)a(M.id,M.component)};if(e)return s(l,e),()=>{a(l,e)}},[l,e,s,a])}function ue({modals:t}){return G(t),null}function ce(t){let{open:e,close:s,isOpen:a}=L();return(0,r.useMemo)(()=>({open:(...l)=>e(t,...l),close:l=>s(t,l),isOpen:()=>a(t)}),[e,s,a,t])}function pe(t){return(0,r.useContext)(B).get(t)?.isOpen??!1}function Me(){return L().closeAll}function L(){let t=(0,r.useContext)(_);if(!t)throw new Error("useModalContext must be used within <ModalProvider>");return t}0&&(module.exports={ModalProvider,ModalRegistry,isBrowser,useCloseAll,useIsModalOpen,useModal,useRegisterModal});
|
|
321
2
|
//# sourceMappingURL=index.cjs.map
|