react-modal-registry 0.1.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/LICENSE +22 -0
- package/README.md +227 -0
- package/dist/index.cjs +321 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +153 -0
- package/dist/index.d.ts +153 -0
- package/dist/index.js +302 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Saurav Hathi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# react-modal-registry
|
|
2
|
+
|
|
3
|
+
> Type-safe, promise-based modal management and registration for React.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/react-modal-registry)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://bundlephobia.com/package/react-modal-registry)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add react-modal-registry
|
|
16
|
+
# or
|
|
17
|
+
npm install react-modal-registry
|
|
18
|
+
# or
|
|
19
|
+
yarn add react-modal-registry
|
|
20
|
+
# or
|
|
21
|
+
bun add react-modal-registry
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### 1. Mount `ModalProvider`
|
|
29
|
+
|
|
30
|
+
Wrap your application root or layout shell:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { ModalProvider } from 'react-modal-registry'
|
|
34
|
+
|
|
35
|
+
export function RootLayout({ children }: { children: React.ReactNode }) {
|
|
36
|
+
return <ModalProvider>{children}</ModalProvider>
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Declare Modal Types
|
|
41
|
+
|
|
42
|
+
In an ambient declaration file (e.g., `env.d.ts` or `modals.d.ts`), augment `ModalDataMap` and `ModalResultMap`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
declare module 'react-modal-registry' {
|
|
46
|
+
interface ModalDataMap {
|
|
47
|
+
confirmDelete: { userId: string; name: string }
|
|
48
|
+
barcodeScanner: { onScan: (code: string) => void }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ModalResultMap {
|
|
52
|
+
confirmDelete: boolean
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Create a Modal Component
|
|
58
|
+
|
|
59
|
+
Write your modal as a standard React component using `ModalProps<TData, TResult>`:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import type { ModalProps } from 'react-modal-registry'
|
|
63
|
+
|
|
64
|
+
export function ConfirmDeleteModal({
|
|
65
|
+
isOpen,
|
|
66
|
+
data,
|
|
67
|
+
close
|
|
68
|
+
}: ModalProps<{ userId: string; name: string }, boolean>) {
|
|
69
|
+
if (!isOpen) return null
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<dialog open={isOpen}>
|
|
73
|
+
<h3>Delete {data.name}?</h3>
|
|
74
|
+
<button onClick={() => close(true)}>Confirm</button>
|
|
75
|
+
<button onClick={() => close(false)}>Cancel</button>
|
|
76
|
+
</dialog>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 4. Register Modals
|
|
82
|
+
|
|
83
|
+
Mount registrations at layout or router boundaries:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { ModalRegistry } from 'react-modal-registry'
|
|
87
|
+
import { ConfirmDeleteModal } from './ConfirmDeleteModal'
|
|
88
|
+
|
|
89
|
+
const appModals = [{ id: 'confirmDelete', component: ConfirmDeleteModal }] as const
|
|
90
|
+
|
|
91
|
+
export function AppShell({ children }: { children: React.ReactNode }) {
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<ModalRegistry modals={appModals} />
|
|
95
|
+
{children}
|
|
96
|
+
</>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 5. Trigger Anywhere with `useModal`
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { useModal } from 'react-modal-registry'
|
|
105
|
+
|
|
106
|
+
export function DeleteButton({ user }: { user: { id: string; name: string } }) {
|
|
107
|
+
const confirmModal = useModal('confirmDelete')
|
|
108
|
+
|
|
109
|
+
const handleDelete = async () => {
|
|
110
|
+
// Typed payload and strictly typed return value (Promise<boolean>)
|
|
111
|
+
const confirmed = await confirmModal.open({
|
|
112
|
+
userId: user.id,
|
|
113
|
+
name: user.name
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
if (confirmed) {
|
|
117
|
+
deleteUser(user.id)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return <button onClick={handleDelete}>Delete User</button>
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Features
|
|
128
|
+
|
|
129
|
+
- **End-to-End Type Safety**: IDE autocompletion for modal IDs, payloads, and return types via TypeScript declaration merging.
|
|
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).
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Why react-modal-registry?
|
|
138
|
+
|
|
139
|
+
| Feature | `react-modal-registry` | `@ebay/nice-modal-react` | `react-modal-promise` | `react-modal-hook` |
|
|
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 |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Patterns
|
|
152
|
+
|
|
153
|
+
### Local Component Registrations
|
|
154
|
+
|
|
155
|
+
Register modals scoped to the lifetime of a specific feature or page with `useRegisterModal`. Registrations unmount automatically when the caller unmounts:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { useRegisterModal } from 'react-modal-registry'
|
|
159
|
+
import { ReportModal } from './ReportModal'
|
|
160
|
+
|
|
161
|
+
export function AnalyticsPage() {
|
|
162
|
+
useRegisterModal('customReport', ReportModal)
|
|
163
|
+
|
|
164
|
+
return <main>Analytics Dashboard</main>
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Callback-Driven Workflows
|
|
169
|
+
|
|
170
|
+
Pass event listeners or streams directly in modal payload data:
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
function ScannerTrigger() {
|
|
174
|
+
const scanner = useModal('barcodeScanner')
|
|
175
|
+
|
|
176
|
+
const handleScan = () => {
|
|
177
|
+
scanner.open({
|
|
178
|
+
onScan: code => console.log('Scanned:', code)
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return <button onClick={handleScan}>Scan Code</button>
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Reactive State Subscriptions
|
|
187
|
+
|
|
188
|
+
`useModal(id).isOpen()` reads state imperatively without re-rendering the caller. Use `useIsModalOpen(id)` when you need reactive UI updates:
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
import { useIsModalOpen } from 'react-modal-registry'
|
|
192
|
+
|
|
193
|
+
function StatusIndicator() {
|
|
194
|
+
const isOpen = useIsModalOpen('confirmDelete')
|
|
195
|
+
return <span>{isOpen ? 'Active' : 'Idle'}</span>
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## API Reference
|
|
202
|
+
|
|
203
|
+
| Export | Type | Description |
|
|
204
|
+
| :-------------------------------- | :-------- | :---------------------------------------------------------------------- |
|
|
205
|
+
| `<ModalProvider>` | Component | Root provider managing modal lifecycle and rendering active slots. |
|
|
206
|
+
| `<ModalRegistry>` | Component | Declarative component registering a batch of modals at root boundaries. |
|
|
207
|
+
| `useModal(id)` | Hook | Returns typed imperative controls (`open`, `close`, `isOpen`). |
|
|
208
|
+
| `useIsModalOpen(id)` | Hook | Reactively subscribes to the open status of a specific modal ID. |
|
|
209
|
+
| `useRegisterModal(id, component)` | Hook | Dynamically registers modals for the lifetime of the calling component. |
|
|
210
|
+
| `useCloseAll()` | Hook | Dismisses all currently open modals. |
|
|
211
|
+
| `ModalProps<TData, TResult>` | Type | Standard props received by registered modal components. |
|
|
212
|
+
| `ModalDataMap` | Interface | Augmentation interface for payload types. |
|
|
213
|
+
| `ModalResultMap` | Interface | Augmentation interface for return types. |
|
|
214
|
+
| `ModalRegistry` | Interface | Unified augmentation interface for paired data and result types. |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Architecture
|
|
219
|
+
|
|
220
|
+
- **Isolated Contexts**: Action dispatchers (`open`, `close`, `register`) are kept separate from slot rendering states. Invoking modal methods does not trigger re-renders in calling views.
|
|
221
|
+
- **Concurrent Requests**: Multiple concurrent invocations of `open()` with identical payloads reuse the active pending promise. Invoking `open()` with new parameters supersedes prior pending promises.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
[MIT](./LICENSE) © [Saurav Hathi](https://github.com/sauravhathi)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
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
|
+
});
|
|
321
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.tsx"],"sourcesContent":["'use client'\n\nimport {\n createContext,\n memo,\n Suspense,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ComponentType,\n type ReactNode\n} from 'react'\n\n/**\n * Registry for modal payload types via module augmentation.\n *\n * @example\n * ```ts\n * declare module 'react-modal-registry' {\n * interface ModalDataMap {\n * confirmAction: { itemId: string }\n * }\n * }\n * ```\n */\nexport interface ModalDataMap {}\n\n/**\n * Registry for modal return types via module augmentation.\n *\n * @example\n * ```ts\n * declare module 'react-modal-registry' {\n * interface ModalResultMap {\n * confirmAction: boolean\n * }\n * }\n * ```\n */\nexport interface ModalResultMap {}\n\n/**\n * Registry for paired modal payload and return types via module augmentation.\n *\n * @example\n * ```ts\n * declare module 'react-modal-registry' {\n * interface ModalRegistry {\n * confirmAction: { data: { itemId: string }; result: boolean }\n * }\n * }\n * ```\n */\nexport interface ModalRegistry {}\n\nexport type RegisteredModalId = keyof ModalDataMap | keyof ModalResultMap | keyof ModalRegistry\n\n/**\n * Modal identifier supporting autocompletion of registered IDs with string fallback.\n */\nexport type ModalId = RegisteredModalId | (string & {})\n\nexport type ModalData<TId extends ModalId> = TId extends keyof ModalDataMap\n ? ModalDataMap[TId]\n : TId extends keyof ModalRegistry\n ? ModalRegistry[TId] extends { data: infer TData }\n ? TData\n : unknown\n : unknown\n\nexport type ModalResult<TId extends ModalId> = TId extends keyof ModalResultMap\n ? ModalResultMap[TId]\n : TId extends keyof ModalRegistry\n ? ModalRegistry[TId] extends { result: infer TResult }\n ? TResult\n : unknown\n : unknown\n\n/**\n * optional vs required payload arguments based on `TData`.\n */\nexport type ModalDataArg<TData> = [unknown] extends [TData]\n ? [data?: TData]\n : [TData] extends [void] | [undefined]\n ? [data?: TData]\n : undefined extends TData\n ? [data?: TData]\n : [data: TData]\n\n/**\n * Standard props passed to registered modal components.\n */\nexport interface ModalProps<TData = unknown, TResult = unknown> {\n isOpen: boolean\n data: TData\n close: (result?: TResult) => void\n}\n\nexport type ModalComponent<TData = unknown, TResult = unknown> =\n | ComponentType<ModalProps<TData, TResult>>\n | ComponentType<ModalProps<TData, unknown>>\n\nexport interface ModalRegistration<TId extends ModalId = ModalId> {\n id: TId\n component: ModalComponent<ModalData<TId>, ModalResult<TId>>\n}\n\n/**\n * Imperative modal controller returned by `useModal(id)`.\n */\nexport interface UseModalReturn<TData = unknown, TResult = unknown> {\n /**\n * Opens the modal with the specified payload and resolves when closed.\n */\n open(...args: ModalDataArg<TData>): Promise<TResult>\n\n /**\n * Closes the modal imperatively, optionally passing a resolution value.\n */\n close(result?: TResult): void\n\n /**\n * Imperatively reads whether the modal is currently open.\n */\n isOpen(): boolean\n}\n\ninterface ModalSlot {\n id: ModalId\n component: ComponentType<ModalProps<unknown, unknown>>\n isOpen: boolean\n isMounted: boolean\n data: unknown\n}\n\ninterface ModalPending {\n promise: Promise<unknown>\n resolve: (value: unknown) => void\n}\n\nexport interface ModalActionsValue {\n register<TId extends ModalId>(\n id: TId,\n component: ModalComponent<ModalData<TId>, ModalResult<TId>>\n ): void\n register<const TIds extends readonly ModalId[]>(modals: {\n [K in keyof TIds]: ModalRegistration<TIds[K]>\n }): void\n unregister(idOrIds: ModalId | readonly ModalId[]): void\n open<TId extends ModalId, TResult = ModalResult<TId>>(\n id: TId,\n ...args: ModalDataArg<ModalData<TId>>\n ): Promise<TResult>\n close<TId extends ModalId>(id: TId, result?: ModalResult<TId>): void\n closeAll(): void\n isOpen(id: ModalId): boolean\n}\n\nconst ModalActionsContext = createContext<ModalActionsValue | null>(null)\nconst ModalSlotsContext = createContext<ReadonlyMap<ModalId, ModalSlot> | null>(null)\n\n// Allows exit animation transitions to complete before unmounting slot\nconst UNMOUNT_DELAY_MS = 200\n\n/**\n * Root provider hosting the modal registry, lifecycle actions, and slot renderer.\n */\nexport function ModalProvider({ children }: { children: ReactNode }) {\n const slotsRef = useRef<Map<ModalId, ModalSlot>>(new Map())\n const pendingRef = useRef<Map<ModalId, ModalPending>>(new Map())\n const timersRef = useRef<Map<ModalId, ReturnType<typeof setTimeout>>>(new Map())\n const [slots, setSlots] = useState<ReadonlyMap<ModalId, ModalSlot>>(new Map())\n\n useEffect(() => {\n const timers = timersRef.current\n const pendings = pendingRef.current\n return () => {\n for (const timer of timers.values()) {\n clearTimeout(timer)\n }\n timers.clear()\n for (const pending of pendings.values()) {\n pending.resolve(undefined)\n }\n pendings.clear()\n }\n }, [])\n\n const sync = useCallback(() => {\n setSlots(new Map(slotsRef.current))\n }, [])\n\n const register: ModalActionsValue['register'] = useCallback(\n (\n idOrModals: ModalId | readonly ModalRegistration[],\n component?: ModalComponent<unknown, unknown>\n ) => {\n const items: readonly ModalRegistration[] = Array.isArray(idOrModals)\n ? idOrModals\n : component\n ? [\n {\n id: idOrModals,\n component: component as ModalComponent<unknown, unknown>\n }\n ]\n : []\n\n let changed = false\n for (const { id, component: comp } of items) {\n const existing = slotsRef.current.get(id)\n const typedComp = comp as ComponentType<ModalProps<unknown, unknown>>\n\n if (existing) {\n if (existing.component !== typedComp) {\n slotsRef.current.set(id, {\n ...existing,\n component: typedComp\n })\n changed = true\n }\n } else {\n slotsRef.current.set(id, {\n id,\n component: typedComp,\n isOpen: false,\n isMounted: false,\n data: undefined\n })\n changed = true\n }\n }\n\n if (changed) {\n sync()\n }\n },\n [sync]\n )\n\n const unregister = useCallback(\n (idOrIds: ModalId | readonly ModalId[]) => {\n const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds]\n let changed = false\n for (const id of ids) {\n const timer = timersRef.current.get(id)\n if (timer) {\n clearTimeout(timer)\n timersRef.current.delete(id)\n }\n\n const pending = pendingRef.current.get(id)\n if (pending) {\n pendingRef.current.delete(id)\n pending.resolve(undefined)\n }\n if (slotsRef.current.has(id)) {\n slotsRef.current.delete(id)\n changed = true\n }\n }\n if (changed) {\n sync()\n }\n },\n [sync]\n )\n\n const open = useCallback(\n <TId extends ModalId, TResult = ModalResult<TId>>(\n id: TId,\n ...args: ModalDataArg<ModalData<TId>>\n ): Promise<TResult> => {\n const data = args[0] as ModalData<TId>\n const slot = slotsRef.current.get(id)\n\n if (!slot) {\n return Promise.reject(\n new Error(`[react-modal-registry] Modal \"${String(id)}\" is not registered.`)\n )\n }\n\n // Re-use active pending promise if open call is repeated with identical payload\n const existing = pendingRef.current.get(id)\n if (slot.isOpen && slot.data === data && existing) {\n return existing.promise as Promise<TResult>\n }\n\n // Supersede any pending promise before creating a new one\n if (existing) {\n pendingRef.current.delete(id)\n existing.resolve(undefined)\n }\n\n const timer = timersRef.current.get(id)\n if (timer) {\n clearTimeout(timer)\n timersRef.current.delete(id)\n }\n\n let resolver!: (value: unknown) => void\n const promise = new Promise<TResult>(resolve => {\n resolver = resolve as (value: unknown) => void\n })\n\n pendingRef.current.set(id, {\n promise: promise as Promise<unknown>,\n resolve: resolver\n })\n\n slotsRef.current.set(id, { ...slot, isOpen: true, isMounted: true, data })\n sync()\n\n return promise\n },\n [sync]\n )\n\n const scheduleUnmount = useCallback(\n (id: ModalId) => {\n const timer = timersRef.current.get(id)\n if (timer) clearTimeout(timer)\n\n timersRef.current.set(\n id,\n setTimeout(() => {\n timersRef.current.delete(id)\n const current = slotsRef.current.get(id)\n if (current && !current.isOpen) {\n slotsRef.current.set(id, { ...current, isMounted: false })\n sync()\n }\n }, UNMOUNT_DELAY_MS)\n )\n },\n [sync]\n )\n\n const close = useCallback(\n <TId extends ModalId>(id: TId, result?: ModalResult<TId>) => {\n const slot = slotsRef.current.get(id)\n if (!slot || !slot.isOpen) return\n\n const pending = pendingRef.current.get(id)\n if (pending) {\n pendingRef.current.delete(id)\n pending.resolve(result)\n }\n\n slotsRef.current.set(id, { ...slot, isOpen: false, isMounted: true })\n sync()\n scheduleUnmount(id)\n },\n [sync, scheduleUnmount]\n )\n\n const closeAll = useCallback(() => {\n for (const [, pending] of pendingRef.current) {\n pending.resolve(undefined)\n }\n pendingRef.current.clear()\n\n let changed = false\n for (const [id, slot] of slotsRef.current) {\n if (slot.isOpen) {\n slotsRef.current.set(id, { ...slot, isOpen: false, isMounted: true })\n scheduleUnmount(id)\n changed = true\n }\n }\n\n if (changed) {\n sync()\n }\n }, [sync, scheduleUnmount])\n\n const isOpen = useCallback((id: ModalId) => slotsRef.current.get(id)?.isOpen ?? false, [])\n\n const actions = useMemo<ModalActionsValue>(\n () => ({\n register,\n unregister,\n open,\n close,\n closeAll,\n isOpen\n }),\n [register, unregister, open, close, closeAll, isOpen]\n )\n\n return (\n <ModalActionsContext.Provider value={actions}>\n <ModalSlotsContext.Provider value={slots}>\n {children}\n <ModalRenderer />\n </ModalSlotsContext.Provider>\n </ModalActionsContext.Provider>\n )\n}\n\n/**\n * Registers one or more modals for the lifetime of the calling component.\n *\n */\nexport function useRegisterModal<TId extends ModalId>(\n id: TId,\n component: ModalComponent<ModalData<TId>, ModalResult<TId>>\n): void\nexport function useRegisterModal<const TIds extends readonly ModalId[]>(modals: {\n [K in keyof TIds]: ModalRegistration<TIds[K]>\n}): void\nexport function useRegisterModal<TId extends ModalId>(\n idOrModals:\n | TId\n | readonly {\n id: ModalId\n component: ComponentType<ModalProps<unknown, unknown>>\n }[],\n component?: ModalComponent<ModalData<TId>, ModalResult<TId>>\n): void {\n const { register, unregister } = useModalContext()\n\n useEffect(() => {\n if (Array.isArray(idOrModals)) {\n register(idOrModals as Parameters<typeof register>[0])\n return () => {\n unregister(idOrModals.map(m => m.id))\n }\n }\n\n if (component) {\n register(idOrModals as TId, component)\n return () => {\n unregister(idOrModals as TId)\n }\n }\n\n return undefined\n }, [idOrModals, component, register, unregister])\n}\n\n/**\n * Declarative component for mounting static modal batches at application or layout roots.\n */\nexport function ModalRegistry<const TIds extends readonly ModalId[]>({\n modals\n}: {\n modals: {\n [K in keyof TIds]: ModalRegistration<TIds[K]>\n }\n}) {\n useRegisterModal(modals)\n return null\n}\n\n/**\n * Returns typed controls (`open`, `close`, `isOpen`) for a modal ID.\n *\n * `isOpen()` reads state imperatively without subscribing the caller to render updates.\n * Use `useIsModalOpen(id)` for reactive subscriptions.\n *\n * @example\n * ```tsx\n * function DeleteButton({ id }: { id: string }) {\n * const confirmModal = useModal('confirmation')\n *\n * const handleDelete = async () => {\n * const confirmed = await confirmModal.open({ itemId: id })\n * if (confirmed) {\n * deleteItem(id)\n * }\n * }\n *\n * return <button onClick={handleDelete}>Delete</button>\n * }\n * ```\n */\nexport function useModal<TId extends ModalId, TResult = ModalResult<TId>>(\n id: TId\n): UseModalReturn<ModalData<TId>, TResult> {\n const { open, close, isOpen } = useModalContext()\n\n return useMemo<UseModalReturn<ModalData<TId>, TResult>>(\n () => ({\n open: (...args: ModalDataArg<ModalData<TId>>) => open<TId, TResult>(id, ...args),\n close: (result?: TResult) => close(id, result as ModalResult<TId>),\n isOpen: () => isOpen(id)\n }),\n [open, close, isOpen, id]\n )\n}\n\n/**\n * Reactively subscribes to whether a modal is currently open.\n */\nexport function useIsModalOpen(id: ModalId): boolean {\n const slots = useContext(ModalSlotsContext)\n\n if (!slots) {\n throw new Error('useIsModalOpen must be used within <ModalProvider>')\n }\n\n return slots.get(id)?.isOpen ?? false\n}\n\nexport function useCloseAll(): () => void {\n return useModalContext().closeAll\n}\n\nconst ModalSlotItem = memo(function ModalSlotItem({\n id,\n component: Component,\n isOpen,\n data,\n close\n}: {\n id: ModalId\n component: ComponentType<ModalProps<unknown, unknown>>\n isOpen: boolean\n data: unknown\n close: (id: ModalId, result?: unknown) => void\n}) {\n const handleClose = useCallback(\n (result?: unknown) => {\n close(id, result)\n },\n [close, id]\n )\n\n return (\n <Suspense fallback={null}>\n <Component isOpen={isOpen} data={data} close={handleClose} />\n </Suspense>\n )\n})\n\nfunction ModalRenderer() {\n const slots = useContext(ModalSlotsContext)\n const { close } = useModalContext()\n\n if (!slots) {\n return null\n }\n\n return (\n <>\n {Array.from(slots.values()).map(slot =>\n slot.isMounted ? (\n <ModalSlotItem\n key={slot.id}\n id={slot.id}\n component={slot.component}\n isOpen={slot.isOpen}\n data={slot.data}\n close={close}\n />\n ) : null\n )}\n </>\n )\n}\n\nfunction useModalContext(): ModalActionsValue {\n const context = useContext(ModalActionsContext)\n\n if (!context) {\n throw new Error('useModalContext must be used within <ModalProvider>')\n }\n\n return context\n}\n"],"mappings":"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","names":["ModalSlotItem"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentType, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Registry for modal payload types via module augmentation.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* declare module 'react-modal-registry' {
|
|
10
|
+
* interface ModalDataMap {
|
|
11
|
+
* confirmAction: { itemId: string }
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
interface ModalDataMap {
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Registry for modal return types via module augmentation.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* declare module 'react-modal-registry' {
|
|
24
|
+
* interface ModalResultMap {
|
|
25
|
+
* confirmAction: boolean
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
interface ModalResultMap {
|
|
31
|
+
}
|
|
32
|
+
type RegisteredModalId = keyof ModalDataMap | keyof ModalResultMap | keyof ModalRegistry;
|
|
33
|
+
/**
|
|
34
|
+
* Modal identifier supporting autocompletion of registered IDs with string fallback.
|
|
35
|
+
*/
|
|
36
|
+
type ModalId = RegisteredModalId | (string & {});
|
|
37
|
+
type ModalData<TId extends ModalId> = TId extends keyof ModalDataMap ? ModalDataMap[TId] : TId extends keyof ModalRegistry ? ModalRegistry[TId] extends {
|
|
38
|
+
data: infer TData;
|
|
39
|
+
} ? TData : unknown : unknown;
|
|
40
|
+
type ModalResult<TId extends ModalId> = TId extends keyof ModalResultMap ? ModalResultMap[TId] : TId extends keyof ModalRegistry ? ModalRegistry[TId] extends {
|
|
41
|
+
result: infer TResult;
|
|
42
|
+
} ? TResult : unknown : unknown;
|
|
43
|
+
/**
|
|
44
|
+
* optional vs required payload arguments based on `TData`.
|
|
45
|
+
*/
|
|
46
|
+
type ModalDataArg<TData> = [unknown] extends [TData] ? [data?: TData] : [TData] extends [void] | [undefined] ? [data?: TData] : undefined extends TData ? [data?: TData] : [data: TData];
|
|
47
|
+
/**
|
|
48
|
+
* Standard props passed to registered modal components.
|
|
49
|
+
*/
|
|
50
|
+
interface ModalProps<TData = unknown, TResult = unknown> {
|
|
51
|
+
isOpen: boolean;
|
|
52
|
+
data: TData;
|
|
53
|
+
close: (result?: TResult) => void;
|
|
54
|
+
}
|
|
55
|
+
type ModalComponent<TData = unknown, TResult = unknown> = ComponentType<ModalProps<TData, TResult>> | ComponentType<ModalProps<TData, unknown>>;
|
|
56
|
+
interface ModalRegistration<TId extends ModalId = ModalId> {
|
|
57
|
+
id: TId;
|
|
58
|
+
component: ModalComponent<ModalData<TId>, ModalResult<TId>>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Imperative modal controller returned by `useModal(id)`.
|
|
62
|
+
*/
|
|
63
|
+
interface UseModalReturn<TData = unknown, TResult = unknown> {
|
|
64
|
+
/**
|
|
65
|
+
* Opens the modal with the specified payload and resolves when closed.
|
|
66
|
+
*/
|
|
67
|
+
open(...args: ModalDataArg<TData>): Promise<TResult>;
|
|
68
|
+
/**
|
|
69
|
+
* Closes the modal imperatively, optionally passing a resolution value.
|
|
70
|
+
*/
|
|
71
|
+
close(result?: TResult): void;
|
|
72
|
+
/**
|
|
73
|
+
* Imperatively reads whether the modal is currently open.
|
|
74
|
+
*/
|
|
75
|
+
isOpen(): boolean;
|
|
76
|
+
}
|
|
77
|
+
interface ModalActionsValue {
|
|
78
|
+
register<TId extends ModalId>(id: TId, component: ModalComponent<ModalData<TId>, ModalResult<TId>>): void;
|
|
79
|
+
register<const TIds extends readonly ModalId[]>(modals: {
|
|
80
|
+
[K in keyof TIds]: ModalRegistration<TIds[K]>;
|
|
81
|
+
}): void;
|
|
82
|
+
unregister(idOrIds: ModalId | readonly ModalId[]): void;
|
|
83
|
+
open<TId extends ModalId, TResult = ModalResult<TId>>(id: TId, ...args: ModalDataArg<ModalData<TId>>): Promise<TResult>;
|
|
84
|
+
close<TId extends ModalId>(id: TId, result?: ModalResult<TId>): void;
|
|
85
|
+
closeAll(): void;
|
|
86
|
+
isOpen(id: ModalId): boolean;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Root provider hosting the modal registry, lifecycle actions, and slot renderer.
|
|
90
|
+
*/
|
|
91
|
+
declare function ModalProvider({ children }: {
|
|
92
|
+
children: ReactNode;
|
|
93
|
+
}): react.JSX.Element;
|
|
94
|
+
/**
|
|
95
|
+
* Registers one or more modals for the lifetime of the calling component.
|
|
96
|
+
*
|
|
97
|
+
*/
|
|
98
|
+
declare function useRegisterModal<TId extends ModalId>(id: TId, component: ModalComponent<ModalData<TId>, ModalResult<TId>>): void;
|
|
99
|
+
declare function useRegisterModal<const TIds extends readonly ModalId[]>(modals: {
|
|
100
|
+
[K in keyof TIds]: ModalRegistration<TIds[K]>;
|
|
101
|
+
}): void;
|
|
102
|
+
/**
|
|
103
|
+
* Registry for paired modal payload and return types via module augmentation.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* declare module 'react-modal-registry' {
|
|
108
|
+
* interface ModalRegistry {
|
|
109
|
+
* confirmAction: { data: { itemId: string }; result: boolean }
|
|
110
|
+
* }
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
interface ModalRegistry {
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Declarative component for mounting static modal batches at application or layout roots.
|
|
118
|
+
*/
|
|
119
|
+
declare function ModalRegistry<const TIds extends readonly ModalId[]>({ modals }: {
|
|
120
|
+
modals: {
|
|
121
|
+
[K in keyof TIds]: ModalRegistration<TIds[K]>;
|
|
122
|
+
};
|
|
123
|
+
}): null;
|
|
124
|
+
/**
|
|
125
|
+
* Returns typed controls (`open`, `close`, `isOpen`) for a modal ID.
|
|
126
|
+
*
|
|
127
|
+
* `isOpen()` reads state imperatively without subscribing the caller to render updates.
|
|
128
|
+
* Use `useIsModalOpen(id)` for reactive subscriptions.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```tsx
|
|
132
|
+
* function DeleteButton({ id }: { id: string }) {
|
|
133
|
+
* const confirmModal = useModal('confirmation')
|
|
134
|
+
*
|
|
135
|
+
* const handleDelete = async () => {
|
|
136
|
+
* const confirmed = await confirmModal.open({ itemId: id })
|
|
137
|
+
* if (confirmed) {
|
|
138
|
+
* deleteItem(id)
|
|
139
|
+
* }
|
|
140
|
+
* }
|
|
141
|
+
*
|
|
142
|
+
* return <button onClick={handleDelete}>Delete</button>
|
|
143
|
+
* }
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
declare function useModal<TId extends ModalId, TResult = ModalResult<TId>>(id: TId): UseModalReturn<ModalData<TId>, TResult>;
|
|
147
|
+
/**
|
|
148
|
+
* Reactively subscribes to whether a modal is currently open.
|
|
149
|
+
*/
|
|
150
|
+
declare function useIsModalOpen(id: ModalId): boolean;
|
|
151
|
+
declare function useCloseAll(): () => void;
|
|
152
|
+
|
|
153
|
+
export { type ModalActionsValue, type ModalComponent, type ModalData, type ModalDataArg, type ModalDataMap, type ModalId, type ModalProps, ModalProvider, type ModalRegistration, ModalRegistry, type ModalResult, type ModalResultMap, type RegisteredModalId, type UseModalReturn, useCloseAll, useIsModalOpen, useModal, useRegisterModal };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentType, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Registry for modal payload types via module augmentation.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* declare module 'react-modal-registry' {
|
|
10
|
+
* interface ModalDataMap {
|
|
11
|
+
* confirmAction: { itemId: string }
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
interface ModalDataMap {
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Registry for modal return types via module augmentation.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* declare module 'react-modal-registry' {
|
|
24
|
+
* interface ModalResultMap {
|
|
25
|
+
* confirmAction: boolean
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
interface ModalResultMap {
|
|
31
|
+
}
|
|
32
|
+
type RegisteredModalId = keyof ModalDataMap | keyof ModalResultMap | keyof ModalRegistry;
|
|
33
|
+
/**
|
|
34
|
+
* Modal identifier supporting autocompletion of registered IDs with string fallback.
|
|
35
|
+
*/
|
|
36
|
+
type ModalId = RegisteredModalId | (string & {});
|
|
37
|
+
type ModalData<TId extends ModalId> = TId extends keyof ModalDataMap ? ModalDataMap[TId] : TId extends keyof ModalRegistry ? ModalRegistry[TId] extends {
|
|
38
|
+
data: infer TData;
|
|
39
|
+
} ? TData : unknown : unknown;
|
|
40
|
+
type ModalResult<TId extends ModalId> = TId extends keyof ModalResultMap ? ModalResultMap[TId] : TId extends keyof ModalRegistry ? ModalRegistry[TId] extends {
|
|
41
|
+
result: infer TResult;
|
|
42
|
+
} ? TResult : unknown : unknown;
|
|
43
|
+
/**
|
|
44
|
+
* optional vs required payload arguments based on `TData`.
|
|
45
|
+
*/
|
|
46
|
+
type ModalDataArg<TData> = [unknown] extends [TData] ? [data?: TData] : [TData] extends [void] | [undefined] ? [data?: TData] : undefined extends TData ? [data?: TData] : [data: TData];
|
|
47
|
+
/**
|
|
48
|
+
* Standard props passed to registered modal components.
|
|
49
|
+
*/
|
|
50
|
+
interface ModalProps<TData = unknown, TResult = unknown> {
|
|
51
|
+
isOpen: boolean;
|
|
52
|
+
data: TData;
|
|
53
|
+
close: (result?: TResult) => void;
|
|
54
|
+
}
|
|
55
|
+
type ModalComponent<TData = unknown, TResult = unknown> = ComponentType<ModalProps<TData, TResult>> | ComponentType<ModalProps<TData, unknown>>;
|
|
56
|
+
interface ModalRegistration<TId extends ModalId = ModalId> {
|
|
57
|
+
id: TId;
|
|
58
|
+
component: ModalComponent<ModalData<TId>, ModalResult<TId>>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Imperative modal controller returned by `useModal(id)`.
|
|
62
|
+
*/
|
|
63
|
+
interface UseModalReturn<TData = unknown, TResult = unknown> {
|
|
64
|
+
/**
|
|
65
|
+
* Opens the modal with the specified payload and resolves when closed.
|
|
66
|
+
*/
|
|
67
|
+
open(...args: ModalDataArg<TData>): Promise<TResult>;
|
|
68
|
+
/**
|
|
69
|
+
* Closes the modal imperatively, optionally passing a resolution value.
|
|
70
|
+
*/
|
|
71
|
+
close(result?: TResult): void;
|
|
72
|
+
/**
|
|
73
|
+
* Imperatively reads whether the modal is currently open.
|
|
74
|
+
*/
|
|
75
|
+
isOpen(): boolean;
|
|
76
|
+
}
|
|
77
|
+
interface ModalActionsValue {
|
|
78
|
+
register<TId extends ModalId>(id: TId, component: ModalComponent<ModalData<TId>, ModalResult<TId>>): void;
|
|
79
|
+
register<const TIds extends readonly ModalId[]>(modals: {
|
|
80
|
+
[K in keyof TIds]: ModalRegistration<TIds[K]>;
|
|
81
|
+
}): void;
|
|
82
|
+
unregister(idOrIds: ModalId | readonly ModalId[]): void;
|
|
83
|
+
open<TId extends ModalId, TResult = ModalResult<TId>>(id: TId, ...args: ModalDataArg<ModalData<TId>>): Promise<TResult>;
|
|
84
|
+
close<TId extends ModalId>(id: TId, result?: ModalResult<TId>): void;
|
|
85
|
+
closeAll(): void;
|
|
86
|
+
isOpen(id: ModalId): boolean;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Root provider hosting the modal registry, lifecycle actions, and slot renderer.
|
|
90
|
+
*/
|
|
91
|
+
declare function ModalProvider({ children }: {
|
|
92
|
+
children: ReactNode;
|
|
93
|
+
}): react.JSX.Element;
|
|
94
|
+
/**
|
|
95
|
+
* Registers one or more modals for the lifetime of the calling component.
|
|
96
|
+
*
|
|
97
|
+
*/
|
|
98
|
+
declare function useRegisterModal<TId extends ModalId>(id: TId, component: ModalComponent<ModalData<TId>, ModalResult<TId>>): void;
|
|
99
|
+
declare function useRegisterModal<const TIds extends readonly ModalId[]>(modals: {
|
|
100
|
+
[K in keyof TIds]: ModalRegistration<TIds[K]>;
|
|
101
|
+
}): void;
|
|
102
|
+
/**
|
|
103
|
+
* Registry for paired modal payload and return types via module augmentation.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* declare module 'react-modal-registry' {
|
|
108
|
+
* interface ModalRegistry {
|
|
109
|
+
* confirmAction: { data: { itemId: string }; result: boolean }
|
|
110
|
+
* }
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
interface ModalRegistry {
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Declarative component for mounting static modal batches at application or layout roots.
|
|
118
|
+
*/
|
|
119
|
+
declare function ModalRegistry<const TIds extends readonly ModalId[]>({ modals }: {
|
|
120
|
+
modals: {
|
|
121
|
+
[K in keyof TIds]: ModalRegistration<TIds[K]>;
|
|
122
|
+
};
|
|
123
|
+
}): null;
|
|
124
|
+
/**
|
|
125
|
+
* Returns typed controls (`open`, `close`, `isOpen`) for a modal ID.
|
|
126
|
+
*
|
|
127
|
+
* `isOpen()` reads state imperatively without subscribing the caller to render updates.
|
|
128
|
+
* Use `useIsModalOpen(id)` for reactive subscriptions.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```tsx
|
|
132
|
+
* function DeleteButton({ id }: { id: string }) {
|
|
133
|
+
* const confirmModal = useModal('confirmation')
|
|
134
|
+
*
|
|
135
|
+
* const handleDelete = async () => {
|
|
136
|
+
* const confirmed = await confirmModal.open({ itemId: id })
|
|
137
|
+
* if (confirmed) {
|
|
138
|
+
* deleteItem(id)
|
|
139
|
+
* }
|
|
140
|
+
* }
|
|
141
|
+
*
|
|
142
|
+
* return <button onClick={handleDelete}>Delete</button>
|
|
143
|
+
* }
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
declare function useModal<TId extends ModalId, TResult = ModalResult<TId>>(id: TId): UseModalReturn<ModalData<TId>, TResult>;
|
|
147
|
+
/**
|
|
148
|
+
* Reactively subscribes to whether a modal is currently open.
|
|
149
|
+
*/
|
|
150
|
+
declare function useIsModalOpen(id: ModalId): boolean;
|
|
151
|
+
declare function useCloseAll(): () => void;
|
|
152
|
+
|
|
153
|
+
export { type ModalActionsValue, type ModalComponent, type ModalData, type ModalDataArg, type ModalDataMap, type ModalId, type ModalProps, ModalProvider, type ModalRegistration, ModalRegistry, type ModalResult, type ModalResultMap, type RegisteredModalId, type UseModalReturn, useCloseAll, useIsModalOpen, useModal, useRegisterModal };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/index.tsx
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
memo,
|
|
7
|
+
Suspense,
|
|
8
|
+
useCallback,
|
|
9
|
+
useContext,
|
|
10
|
+
useEffect,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef,
|
|
13
|
+
useState
|
|
14
|
+
} from "react";
|
|
15
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
16
|
+
var ModalActionsContext = createContext(null);
|
|
17
|
+
var ModalSlotsContext = createContext(null);
|
|
18
|
+
var UNMOUNT_DELAY_MS = 200;
|
|
19
|
+
function ModalProvider({ children }) {
|
|
20
|
+
const slotsRef = useRef(/* @__PURE__ */ new Map());
|
|
21
|
+
const pendingRef = useRef(/* @__PURE__ */ new Map());
|
|
22
|
+
const timersRef = useRef(/* @__PURE__ */ new Map());
|
|
23
|
+
const [slots, setSlots] = useState(/* @__PURE__ */ new Map());
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const timers = timersRef.current;
|
|
26
|
+
const pendings = pendingRef.current;
|
|
27
|
+
return () => {
|
|
28
|
+
for (const timer of timers.values()) {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
}
|
|
31
|
+
timers.clear();
|
|
32
|
+
for (const pending of pendings.values()) {
|
|
33
|
+
pending.resolve(void 0);
|
|
34
|
+
}
|
|
35
|
+
pendings.clear();
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
const sync = useCallback(() => {
|
|
39
|
+
setSlots(new Map(slotsRef.current));
|
|
40
|
+
}, []);
|
|
41
|
+
const register = useCallback(
|
|
42
|
+
(idOrModals, component) => {
|
|
43
|
+
const items = Array.isArray(idOrModals) ? idOrModals : component ? [
|
|
44
|
+
{
|
|
45
|
+
id: idOrModals,
|
|
46
|
+
component
|
|
47
|
+
}
|
|
48
|
+
] : [];
|
|
49
|
+
let changed = false;
|
|
50
|
+
for (const { id, component: comp } of items) {
|
|
51
|
+
const existing = slotsRef.current.get(id);
|
|
52
|
+
const typedComp = comp;
|
|
53
|
+
if (existing) {
|
|
54
|
+
if (existing.component !== typedComp) {
|
|
55
|
+
slotsRef.current.set(id, {
|
|
56
|
+
...existing,
|
|
57
|
+
component: typedComp
|
|
58
|
+
});
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
slotsRef.current.set(id, {
|
|
63
|
+
id,
|
|
64
|
+
component: typedComp,
|
|
65
|
+
isOpen: false,
|
|
66
|
+
isMounted: false,
|
|
67
|
+
data: void 0
|
|
68
|
+
});
|
|
69
|
+
changed = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (changed) {
|
|
73
|
+
sync();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
[sync]
|
|
77
|
+
);
|
|
78
|
+
const unregister = useCallback(
|
|
79
|
+
(idOrIds) => {
|
|
80
|
+
const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
|
81
|
+
let changed = false;
|
|
82
|
+
for (const id of ids) {
|
|
83
|
+
const timer = timersRef.current.get(id);
|
|
84
|
+
if (timer) {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
timersRef.current.delete(id);
|
|
87
|
+
}
|
|
88
|
+
const pending = pendingRef.current.get(id);
|
|
89
|
+
if (pending) {
|
|
90
|
+
pendingRef.current.delete(id);
|
|
91
|
+
pending.resolve(void 0);
|
|
92
|
+
}
|
|
93
|
+
if (slotsRef.current.has(id)) {
|
|
94
|
+
slotsRef.current.delete(id);
|
|
95
|
+
changed = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (changed) {
|
|
99
|
+
sync();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[sync]
|
|
103
|
+
);
|
|
104
|
+
const open = useCallback(
|
|
105
|
+
(id, ...args) => {
|
|
106
|
+
const data = args[0];
|
|
107
|
+
const slot = slotsRef.current.get(id);
|
|
108
|
+
if (!slot) {
|
|
109
|
+
return Promise.reject(
|
|
110
|
+
new Error(`[react-modal-registry] Modal "${String(id)}" is not registered.`)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const existing = pendingRef.current.get(id);
|
|
114
|
+
if (slot.isOpen && slot.data === data && existing) {
|
|
115
|
+
return existing.promise;
|
|
116
|
+
}
|
|
117
|
+
if (existing) {
|
|
118
|
+
pendingRef.current.delete(id);
|
|
119
|
+
existing.resolve(void 0);
|
|
120
|
+
}
|
|
121
|
+
const timer = timersRef.current.get(id);
|
|
122
|
+
if (timer) {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
timersRef.current.delete(id);
|
|
125
|
+
}
|
|
126
|
+
let resolver;
|
|
127
|
+
const promise = new Promise((resolve) => {
|
|
128
|
+
resolver = resolve;
|
|
129
|
+
});
|
|
130
|
+
pendingRef.current.set(id, {
|
|
131
|
+
promise,
|
|
132
|
+
resolve: resolver
|
|
133
|
+
});
|
|
134
|
+
slotsRef.current.set(id, { ...slot, isOpen: true, isMounted: true, data });
|
|
135
|
+
sync();
|
|
136
|
+
return promise;
|
|
137
|
+
},
|
|
138
|
+
[sync]
|
|
139
|
+
);
|
|
140
|
+
const scheduleUnmount = useCallback(
|
|
141
|
+
(id) => {
|
|
142
|
+
const timer = timersRef.current.get(id);
|
|
143
|
+
if (timer) clearTimeout(timer);
|
|
144
|
+
timersRef.current.set(
|
|
145
|
+
id,
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
timersRef.current.delete(id);
|
|
148
|
+
const current = slotsRef.current.get(id);
|
|
149
|
+
if (current && !current.isOpen) {
|
|
150
|
+
slotsRef.current.set(id, { ...current, isMounted: false });
|
|
151
|
+
sync();
|
|
152
|
+
}
|
|
153
|
+
}, UNMOUNT_DELAY_MS)
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
[sync]
|
|
157
|
+
);
|
|
158
|
+
const close = useCallback(
|
|
159
|
+
(id, result) => {
|
|
160
|
+
const slot = slotsRef.current.get(id);
|
|
161
|
+
if (!slot || !slot.isOpen) return;
|
|
162
|
+
const pending = pendingRef.current.get(id);
|
|
163
|
+
if (pending) {
|
|
164
|
+
pendingRef.current.delete(id);
|
|
165
|
+
pending.resolve(result);
|
|
166
|
+
}
|
|
167
|
+
slotsRef.current.set(id, { ...slot, isOpen: false, isMounted: true });
|
|
168
|
+
sync();
|
|
169
|
+
scheduleUnmount(id);
|
|
170
|
+
},
|
|
171
|
+
[sync, scheduleUnmount]
|
|
172
|
+
);
|
|
173
|
+
const closeAll = useCallback(() => {
|
|
174
|
+
for (const [, pending] of pendingRef.current) {
|
|
175
|
+
pending.resolve(void 0);
|
|
176
|
+
}
|
|
177
|
+
pendingRef.current.clear();
|
|
178
|
+
let changed = false;
|
|
179
|
+
for (const [id, slot] of slotsRef.current) {
|
|
180
|
+
if (slot.isOpen) {
|
|
181
|
+
slotsRef.current.set(id, { ...slot, isOpen: false, isMounted: true });
|
|
182
|
+
scheduleUnmount(id);
|
|
183
|
+
changed = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (changed) {
|
|
187
|
+
sync();
|
|
188
|
+
}
|
|
189
|
+
}, [sync, scheduleUnmount]);
|
|
190
|
+
const isOpen = useCallback((id) => slotsRef.current.get(id)?.isOpen ?? false, []);
|
|
191
|
+
const actions = useMemo(
|
|
192
|
+
() => ({
|
|
193
|
+
register,
|
|
194
|
+
unregister,
|
|
195
|
+
open,
|
|
196
|
+
close,
|
|
197
|
+
closeAll,
|
|
198
|
+
isOpen
|
|
199
|
+
}),
|
|
200
|
+
[register, unregister, open, close, closeAll, isOpen]
|
|
201
|
+
);
|
|
202
|
+
return /* @__PURE__ */ jsx(ModalActionsContext.Provider, { value: actions, children: /* @__PURE__ */ jsxs(ModalSlotsContext.Provider, { value: slots, children: [
|
|
203
|
+
children,
|
|
204
|
+
/* @__PURE__ */ jsx(ModalRenderer, {})
|
|
205
|
+
] }) });
|
|
206
|
+
}
|
|
207
|
+
function useRegisterModal(idOrModals, component) {
|
|
208
|
+
const { register, unregister } = useModalContext();
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (Array.isArray(idOrModals)) {
|
|
211
|
+
register(idOrModals);
|
|
212
|
+
return () => {
|
|
213
|
+
unregister(idOrModals.map((m) => m.id));
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (component) {
|
|
217
|
+
register(idOrModals, component);
|
|
218
|
+
return () => {
|
|
219
|
+
unregister(idOrModals);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return void 0;
|
|
223
|
+
}, [idOrModals, component, register, unregister]);
|
|
224
|
+
}
|
|
225
|
+
function ModalRegistry({
|
|
226
|
+
modals
|
|
227
|
+
}) {
|
|
228
|
+
useRegisterModal(modals);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
function useModal(id) {
|
|
232
|
+
const { open, close, isOpen } = useModalContext();
|
|
233
|
+
return useMemo(
|
|
234
|
+
() => ({
|
|
235
|
+
open: (...args) => open(id, ...args),
|
|
236
|
+
close: (result) => close(id, result),
|
|
237
|
+
isOpen: () => isOpen(id)
|
|
238
|
+
}),
|
|
239
|
+
[open, close, isOpen, id]
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
function useIsModalOpen(id) {
|
|
243
|
+
const slots = useContext(ModalSlotsContext);
|
|
244
|
+
if (!slots) {
|
|
245
|
+
throw new Error("useIsModalOpen must be used within <ModalProvider>");
|
|
246
|
+
}
|
|
247
|
+
return slots.get(id)?.isOpen ?? false;
|
|
248
|
+
}
|
|
249
|
+
function useCloseAll() {
|
|
250
|
+
return useModalContext().closeAll;
|
|
251
|
+
}
|
|
252
|
+
var ModalSlotItem = memo(function ModalSlotItem2({
|
|
253
|
+
id,
|
|
254
|
+
component: Component,
|
|
255
|
+
isOpen,
|
|
256
|
+
data,
|
|
257
|
+
close
|
|
258
|
+
}) {
|
|
259
|
+
const handleClose = useCallback(
|
|
260
|
+
(result) => {
|
|
261
|
+
close(id, result);
|
|
262
|
+
},
|
|
263
|
+
[close, id]
|
|
264
|
+
);
|
|
265
|
+
return /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(Component, { isOpen, data, close: handleClose }) });
|
|
266
|
+
});
|
|
267
|
+
function ModalRenderer() {
|
|
268
|
+
const slots = useContext(ModalSlotsContext);
|
|
269
|
+
const { close } = useModalContext();
|
|
270
|
+
if (!slots) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return /* @__PURE__ */ jsx(Fragment, { children: Array.from(slots.values()).map(
|
|
274
|
+
(slot) => slot.isMounted ? /* @__PURE__ */ jsx(
|
|
275
|
+
ModalSlotItem,
|
|
276
|
+
{
|
|
277
|
+
id: slot.id,
|
|
278
|
+
component: slot.component,
|
|
279
|
+
isOpen: slot.isOpen,
|
|
280
|
+
data: slot.data,
|
|
281
|
+
close
|
|
282
|
+
},
|
|
283
|
+
slot.id
|
|
284
|
+
) : null
|
|
285
|
+
) });
|
|
286
|
+
}
|
|
287
|
+
function useModalContext() {
|
|
288
|
+
const context = useContext(ModalActionsContext);
|
|
289
|
+
if (!context) {
|
|
290
|
+
throw new Error("useModalContext must be used within <ModalProvider>");
|
|
291
|
+
}
|
|
292
|
+
return context;
|
|
293
|
+
}
|
|
294
|
+
export {
|
|
295
|
+
ModalProvider,
|
|
296
|
+
ModalRegistry,
|
|
297
|
+
useCloseAll,
|
|
298
|
+
useIsModalOpen,
|
|
299
|
+
useModal,
|
|
300
|
+
useRegisterModal
|
|
301
|
+
};
|
|
302
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.tsx"],"sourcesContent":["'use client'\n\nimport {\n createContext,\n memo,\n Suspense,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ComponentType,\n type ReactNode\n} from 'react'\n\n/**\n * Registry for modal payload types via module augmentation.\n *\n * @example\n * ```ts\n * declare module 'react-modal-registry' {\n * interface ModalDataMap {\n * confirmAction: { itemId: string }\n * }\n * }\n * ```\n */\nexport interface ModalDataMap {}\n\n/**\n * Registry for modal return types via module augmentation.\n *\n * @example\n * ```ts\n * declare module 'react-modal-registry' {\n * interface ModalResultMap {\n * confirmAction: boolean\n * }\n * }\n * ```\n */\nexport interface ModalResultMap {}\n\n/**\n * Registry for paired modal payload and return types via module augmentation.\n *\n * @example\n * ```ts\n * declare module 'react-modal-registry' {\n * interface ModalRegistry {\n * confirmAction: { data: { itemId: string }; result: boolean }\n * }\n * }\n * ```\n */\nexport interface ModalRegistry {}\n\nexport type RegisteredModalId = keyof ModalDataMap | keyof ModalResultMap | keyof ModalRegistry\n\n/**\n * Modal identifier supporting autocompletion of registered IDs with string fallback.\n */\nexport type ModalId = RegisteredModalId | (string & {})\n\nexport type ModalData<TId extends ModalId> = TId extends keyof ModalDataMap\n ? ModalDataMap[TId]\n : TId extends keyof ModalRegistry\n ? ModalRegistry[TId] extends { data: infer TData }\n ? TData\n : unknown\n : unknown\n\nexport type ModalResult<TId extends ModalId> = TId extends keyof ModalResultMap\n ? ModalResultMap[TId]\n : TId extends keyof ModalRegistry\n ? ModalRegistry[TId] extends { result: infer TResult }\n ? TResult\n : unknown\n : unknown\n\n/**\n * optional vs required payload arguments based on `TData`.\n */\nexport type ModalDataArg<TData> = [unknown] extends [TData]\n ? [data?: TData]\n : [TData] extends [void] | [undefined]\n ? [data?: TData]\n : undefined extends TData\n ? [data?: TData]\n : [data: TData]\n\n/**\n * Standard props passed to registered modal components.\n */\nexport interface ModalProps<TData = unknown, TResult = unknown> {\n isOpen: boolean\n data: TData\n close: (result?: TResult) => void\n}\n\nexport type ModalComponent<TData = unknown, TResult = unknown> =\n | ComponentType<ModalProps<TData, TResult>>\n | ComponentType<ModalProps<TData, unknown>>\n\nexport interface ModalRegistration<TId extends ModalId = ModalId> {\n id: TId\n component: ModalComponent<ModalData<TId>, ModalResult<TId>>\n}\n\n/**\n * Imperative modal controller returned by `useModal(id)`.\n */\nexport interface UseModalReturn<TData = unknown, TResult = unknown> {\n /**\n * Opens the modal with the specified payload and resolves when closed.\n */\n open(...args: ModalDataArg<TData>): Promise<TResult>\n\n /**\n * Closes the modal imperatively, optionally passing a resolution value.\n */\n close(result?: TResult): void\n\n /**\n * Imperatively reads whether the modal is currently open.\n */\n isOpen(): boolean\n}\n\ninterface ModalSlot {\n id: ModalId\n component: ComponentType<ModalProps<unknown, unknown>>\n isOpen: boolean\n isMounted: boolean\n data: unknown\n}\n\ninterface ModalPending {\n promise: Promise<unknown>\n resolve: (value: unknown) => void\n}\n\nexport interface ModalActionsValue {\n register<TId extends ModalId>(\n id: TId,\n component: ModalComponent<ModalData<TId>, ModalResult<TId>>\n ): void\n register<const TIds extends readonly ModalId[]>(modals: {\n [K in keyof TIds]: ModalRegistration<TIds[K]>\n }): void\n unregister(idOrIds: ModalId | readonly ModalId[]): void\n open<TId extends ModalId, TResult = ModalResult<TId>>(\n id: TId,\n ...args: ModalDataArg<ModalData<TId>>\n ): Promise<TResult>\n close<TId extends ModalId>(id: TId, result?: ModalResult<TId>): void\n closeAll(): void\n isOpen(id: ModalId): boolean\n}\n\nconst ModalActionsContext = createContext<ModalActionsValue | null>(null)\nconst ModalSlotsContext = createContext<ReadonlyMap<ModalId, ModalSlot> | null>(null)\n\n// Allows exit animation transitions to complete before unmounting slot\nconst UNMOUNT_DELAY_MS = 200\n\n/**\n * Root provider hosting the modal registry, lifecycle actions, and slot renderer.\n */\nexport function ModalProvider({ children }: { children: ReactNode }) {\n const slotsRef = useRef<Map<ModalId, ModalSlot>>(new Map())\n const pendingRef = useRef<Map<ModalId, ModalPending>>(new Map())\n const timersRef = useRef<Map<ModalId, ReturnType<typeof setTimeout>>>(new Map())\n const [slots, setSlots] = useState<ReadonlyMap<ModalId, ModalSlot>>(new Map())\n\n useEffect(() => {\n const timers = timersRef.current\n const pendings = pendingRef.current\n return () => {\n for (const timer of timers.values()) {\n clearTimeout(timer)\n }\n timers.clear()\n for (const pending of pendings.values()) {\n pending.resolve(undefined)\n }\n pendings.clear()\n }\n }, [])\n\n const sync = useCallback(() => {\n setSlots(new Map(slotsRef.current))\n }, [])\n\n const register: ModalActionsValue['register'] = useCallback(\n (\n idOrModals: ModalId | readonly ModalRegistration[],\n component?: ModalComponent<unknown, unknown>\n ) => {\n const items: readonly ModalRegistration[] = Array.isArray(idOrModals)\n ? idOrModals\n : component\n ? [\n {\n id: idOrModals,\n component: component as ModalComponent<unknown, unknown>\n }\n ]\n : []\n\n let changed = false\n for (const { id, component: comp } of items) {\n const existing = slotsRef.current.get(id)\n const typedComp = comp as ComponentType<ModalProps<unknown, unknown>>\n\n if (existing) {\n if (existing.component !== typedComp) {\n slotsRef.current.set(id, {\n ...existing,\n component: typedComp\n })\n changed = true\n }\n } else {\n slotsRef.current.set(id, {\n id,\n component: typedComp,\n isOpen: false,\n isMounted: false,\n data: undefined\n })\n changed = true\n }\n }\n\n if (changed) {\n sync()\n }\n },\n [sync]\n )\n\n const unregister = useCallback(\n (idOrIds: ModalId | readonly ModalId[]) => {\n const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds]\n let changed = false\n for (const id of ids) {\n const timer = timersRef.current.get(id)\n if (timer) {\n clearTimeout(timer)\n timersRef.current.delete(id)\n }\n\n const pending = pendingRef.current.get(id)\n if (pending) {\n pendingRef.current.delete(id)\n pending.resolve(undefined)\n }\n if (slotsRef.current.has(id)) {\n slotsRef.current.delete(id)\n changed = true\n }\n }\n if (changed) {\n sync()\n }\n },\n [sync]\n )\n\n const open = useCallback(\n <TId extends ModalId, TResult = ModalResult<TId>>(\n id: TId,\n ...args: ModalDataArg<ModalData<TId>>\n ): Promise<TResult> => {\n const data = args[0] as ModalData<TId>\n const slot = slotsRef.current.get(id)\n\n if (!slot) {\n return Promise.reject(\n new Error(`[react-modal-registry] Modal \"${String(id)}\" is not registered.`)\n )\n }\n\n // Re-use active pending promise if open call is repeated with identical payload\n const existing = pendingRef.current.get(id)\n if (slot.isOpen && slot.data === data && existing) {\n return existing.promise as Promise<TResult>\n }\n\n // Supersede any pending promise before creating a new one\n if (existing) {\n pendingRef.current.delete(id)\n existing.resolve(undefined)\n }\n\n const timer = timersRef.current.get(id)\n if (timer) {\n clearTimeout(timer)\n timersRef.current.delete(id)\n }\n\n let resolver!: (value: unknown) => void\n const promise = new Promise<TResult>(resolve => {\n resolver = resolve as (value: unknown) => void\n })\n\n pendingRef.current.set(id, {\n promise: promise as Promise<unknown>,\n resolve: resolver\n })\n\n slotsRef.current.set(id, { ...slot, isOpen: true, isMounted: true, data })\n sync()\n\n return promise\n },\n [sync]\n )\n\n const scheduleUnmount = useCallback(\n (id: ModalId) => {\n const timer = timersRef.current.get(id)\n if (timer) clearTimeout(timer)\n\n timersRef.current.set(\n id,\n setTimeout(() => {\n timersRef.current.delete(id)\n const current = slotsRef.current.get(id)\n if (current && !current.isOpen) {\n slotsRef.current.set(id, { ...current, isMounted: false })\n sync()\n }\n }, UNMOUNT_DELAY_MS)\n )\n },\n [sync]\n )\n\n const close = useCallback(\n <TId extends ModalId>(id: TId, result?: ModalResult<TId>) => {\n const slot = slotsRef.current.get(id)\n if (!slot || !slot.isOpen) return\n\n const pending = pendingRef.current.get(id)\n if (pending) {\n pendingRef.current.delete(id)\n pending.resolve(result)\n }\n\n slotsRef.current.set(id, { ...slot, isOpen: false, isMounted: true })\n sync()\n scheduleUnmount(id)\n },\n [sync, scheduleUnmount]\n )\n\n const closeAll = useCallback(() => {\n for (const [, pending] of pendingRef.current) {\n pending.resolve(undefined)\n }\n pendingRef.current.clear()\n\n let changed = false\n for (const [id, slot] of slotsRef.current) {\n if (slot.isOpen) {\n slotsRef.current.set(id, { ...slot, isOpen: false, isMounted: true })\n scheduleUnmount(id)\n changed = true\n }\n }\n\n if (changed) {\n sync()\n }\n }, [sync, scheduleUnmount])\n\n const isOpen = useCallback((id: ModalId) => slotsRef.current.get(id)?.isOpen ?? false, [])\n\n const actions = useMemo<ModalActionsValue>(\n () => ({\n register,\n unregister,\n open,\n close,\n closeAll,\n isOpen\n }),\n [register, unregister, open, close, closeAll, isOpen]\n )\n\n return (\n <ModalActionsContext.Provider value={actions}>\n <ModalSlotsContext.Provider value={slots}>\n {children}\n <ModalRenderer />\n </ModalSlotsContext.Provider>\n </ModalActionsContext.Provider>\n )\n}\n\n/**\n * Registers one or more modals for the lifetime of the calling component.\n *\n */\nexport function useRegisterModal<TId extends ModalId>(\n id: TId,\n component: ModalComponent<ModalData<TId>, ModalResult<TId>>\n): void\nexport function useRegisterModal<const TIds extends readonly ModalId[]>(modals: {\n [K in keyof TIds]: ModalRegistration<TIds[K]>\n}): void\nexport function useRegisterModal<TId extends ModalId>(\n idOrModals:\n | TId\n | readonly {\n id: ModalId\n component: ComponentType<ModalProps<unknown, unknown>>\n }[],\n component?: ModalComponent<ModalData<TId>, ModalResult<TId>>\n): void {\n const { register, unregister } = useModalContext()\n\n useEffect(() => {\n if (Array.isArray(idOrModals)) {\n register(idOrModals as Parameters<typeof register>[0])\n return () => {\n unregister(idOrModals.map(m => m.id))\n }\n }\n\n if (component) {\n register(idOrModals as TId, component)\n return () => {\n unregister(idOrModals as TId)\n }\n }\n\n return undefined\n }, [idOrModals, component, register, unregister])\n}\n\n/**\n * Declarative component for mounting static modal batches at application or layout roots.\n */\nexport function ModalRegistry<const TIds extends readonly ModalId[]>({\n modals\n}: {\n modals: {\n [K in keyof TIds]: ModalRegistration<TIds[K]>\n }\n}) {\n useRegisterModal(modals)\n return null\n}\n\n/**\n * Returns typed controls (`open`, `close`, `isOpen`) for a modal ID.\n *\n * `isOpen()` reads state imperatively without subscribing the caller to render updates.\n * Use `useIsModalOpen(id)` for reactive subscriptions.\n *\n * @example\n * ```tsx\n * function DeleteButton({ id }: { id: string }) {\n * const confirmModal = useModal('confirmation')\n *\n * const handleDelete = async () => {\n * const confirmed = await confirmModal.open({ itemId: id })\n * if (confirmed) {\n * deleteItem(id)\n * }\n * }\n *\n * return <button onClick={handleDelete}>Delete</button>\n * }\n * ```\n */\nexport function useModal<TId extends ModalId, TResult = ModalResult<TId>>(\n id: TId\n): UseModalReturn<ModalData<TId>, TResult> {\n const { open, close, isOpen } = useModalContext()\n\n return useMemo<UseModalReturn<ModalData<TId>, TResult>>(\n () => ({\n open: (...args: ModalDataArg<ModalData<TId>>) => open<TId, TResult>(id, ...args),\n close: (result?: TResult) => close(id, result as ModalResult<TId>),\n isOpen: () => isOpen(id)\n }),\n [open, close, isOpen, id]\n )\n}\n\n/**\n * Reactively subscribes to whether a modal is currently open.\n */\nexport function useIsModalOpen(id: ModalId): boolean {\n const slots = useContext(ModalSlotsContext)\n\n if (!slots) {\n throw new Error('useIsModalOpen must be used within <ModalProvider>')\n }\n\n return slots.get(id)?.isOpen ?? false\n}\n\nexport function useCloseAll(): () => void {\n return useModalContext().closeAll\n}\n\nconst ModalSlotItem = memo(function ModalSlotItem({\n id,\n component: Component,\n isOpen,\n data,\n close\n}: {\n id: ModalId\n component: ComponentType<ModalProps<unknown, unknown>>\n isOpen: boolean\n data: unknown\n close: (id: ModalId, result?: unknown) => void\n}) {\n const handleClose = useCallback(\n (result?: unknown) => {\n close(id, result)\n },\n [close, id]\n )\n\n return (\n <Suspense fallback={null}>\n <Component isOpen={isOpen} data={data} close={handleClose} />\n </Suspense>\n )\n})\n\nfunction ModalRenderer() {\n const slots = useContext(ModalSlotsContext)\n const { close } = useModalContext()\n\n if (!slots) {\n return null\n }\n\n return (\n <>\n {Array.from(slots.values()).map(slot =>\n slot.isMounted ? (\n <ModalSlotItem\n key={slot.id}\n id={slot.id}\n component={slot.component}\n isOpen={slot.isOpen}\n data={slot.data}\n close={close}\n />\n ) : null\n )}\n </>\n )\n}\n\nfunction useModalContext(): ModalActionsValue {\n const context = useContext(ModalActionsContext)\n\n if (!context) {\n throw new Error('useModalContext must be used within <ModalProvider>')\n }\n\n return context\n}\n"],"mappings":"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","names":["ModalSlotItem"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-modal-registry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Type-safe, headless modal registry and promise-based manager for React",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"modal",
|
|
8
|
+
"modals",
|
|
9
|
+
"dialog",
|
|
10
|
+
"manager",
|
|
11
|
+
"registry",
|
|
12
|
+
"promise",
|
|
13
|
+
"headless",
|
|
14
|
+
"typescript"
|
|
15
|
+
],
|
|
16
|
+
"author": "Saurav Hathi <https://github.com/sauravhathi>",
|
|
17
|
+
"homepage": "https://github.com/sauravhathi/react-modal-registry#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/sauravhathi/react-modal-registry.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/sauravhathi/react-modal-registry/issues"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.cjs",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js",
|
|
34
|
+
"require": "./dist/index.cjs"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"prepublishOnly": "pnpm run build"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": ">=19.0.0",
|
|
50
|
+
"react-dom": ">=19.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/react": "^19.2.7",
|
|
54
|
+
"@types/react-dom": "^19.2.3",
|
|
55
|
+
"tsup": "^8.5.1",
|
|
56
|
+
"typescript": "^6.0.2"
|
|
57
|
+
}
|
|
58
|
+
}
|