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 CHANGED
@@ -1,12 +1,23 @@
1
1
  # react-modal-registry
2
2
 
3
- > Type-safe, promise-based modal management and registration for React.
3
+ > Type-safe, promise-based modal and dialog management for React.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/react-modal-registry?style=flat-square&color=black)](https://www.npmjs.com/package/react-modal-registry)
6
6
  [![license](https://img.shields.io/badge/license-MIT-black?style=flat-square)](./LICENSE)
7
7
  [![bundle size](https://img.shields.io/bundlephobia/minzip/react-modal-registry?style=flat-square&color=black)](https://bundlephobia.com/package/react-modal-registry)
8
8
  [![typescript](https://img.shields.io/badge/TypeScript-strict-black?style=flat-square)](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 or layout shell:
41
+ Wrap your application root:
31
42
 
32
43
  ```tsx
33
44
  import { ModalProvider } from 'react-modal-registry'
34
45
 
35
- export function RootLayout({ children }: { children: React.ReactNode }) {
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
- In an ambient declaration file (e.g., `env.d.ts` or `modals.d.ts`), augment `ModalDataMap` and `ModalResultMap`:
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 ModalDataMap {
47
- confirmDelete: { userId: string; name: string }
48
- barcodeScanner: { onScan: (code: string) => void }
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
- Write your modal as a standard React component using `ModalProps<TData, TResult>`:
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<{ userId: string; name: string }, boolean>) {
77
+ }: ModalProps<{ name: string }, boolean>) {
69
78
  if (!isOpen) return null
70
79
 
71
80
  return (
72
81
  <dialog open={isOpen}>
73
- <h3>Delete {data.name}?</h3>
74
- <button onClick={() => close(true)}>Confirm</button>
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 Modals
90
+ ### 4. Register and Open
82
91
 
83
- Mount registrations at layout or router boundaries:
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 appModals = [{ id: 'confirmDelete', component: ConfirmDeleteModal }] as const
98
+ const modals = [{ id: 'confirmDelete', component: ConfirmDeleteModal }] as const
90
99
 
91
- export function AppShell({ children }: { children: React.ReactNode }) {
100
+ export function Layout({ children }: { children: React.ReactNode }) {
92
101
  return (
93
102
  <>
94
- <ModalRegistry modals={appModals} />
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({ user }: { user: { id: string; name: string } }) {
109
+ export function DeleteButton({ itemName }: { itemName: string }) {
107
110
  const confirmModal = useModal('confirmDelete')
108
111
 
109
112
  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
-
113
+ const confirmed = await confirmModal.open({ name: itemName })
116
114
  if (confirmed) {
117
- deleteUser(user.id)
115
+ // item deleted
118
116
  }
119
117
  }
120
118
 
121
- return <button onClick={handleDelete}>Delete User</button>
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
- - **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).
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
- ## Why react-modal-registry?
131
+ ```tsx
132
+ const confirmed = await modal.open({ name: 'Project Alpha' })
133
+ if (confirmed) {
134
+ await api.deleteProject()
135
+ }
136
+ ```
138
137
 
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 |
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
- ## Patterns
142
+ Pay-load data is passed in-memory, so you can pass callbacks, functions, or complex objects directly:
152
143
 
153
- ### Local Component Registrations
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
- Register modals scoped to the lifetime of a specific feature or page with `useRegisterModal`. Registrations unmount automatically when the caller unmounts:
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 { ReportModal } from './ReportModal'
169
+ import { useRegisterModal, useModal } from 'react-modal-registry'
170
+ import { FeatureModal } from './FeatureModal'
160
171
 
161
- export function AnalyticsPage() {
162
- useRegisterModal('customReport', ReportModal)
172
+ export function FeaturePage() {
173
+ useRegisterModal('featureDetails', FeatureModal)
174
+ const modal = useModal('featureDetails')
163
175
 
164
- return <main>Analytics Dashboard</main>
176
+ return <button onClick={() => modal.open()}>Open Details</button>
165
177
  }
166
178
  ```
167
179
 
168
- ### Callback-Driven Workflows
180
+ ### Code Splitting with `React.lazy`
169
181
 
170
- Pass event listeners or streams directly in modal payload data:
182
+ Split modal components into separate chunks:
171
183
 
172
184
  ```tsx
173
- function ScannerTrigger() {
174
- const scanner = useModal('barcodeScanner')
185
+ import { lazy } from 'react'
186
+ import { ModalRegistry } from 'react-modal-registry'
175
187
 
176
- const handleScan = () => {
177
- scanner.open({
178
- onScan: code => console.log('Scanned:', code)
179
- })
180
- }
188
+ const AnalyticsModal = lazy(() => import('./AnalyticsModal'))
189
+
190
+ const modals = [{ id: 'analytics', component: AnalyticsModal }] as const
181
191
 
182
- return <button onClick={handleScan}>Scan Code</button>
192
+ export function App() {
193
+ return <ModalRegistry modals={modals} />
183
194
  }
184
195
  ```
185
196
 
186
- ### Reactive State Subscriptions
197
+ ### URL Deep Linking
187
198
 
188
- `useModal(id).isOpen()` reads state imperatively without re-rendering the caller. Use `useIsModalOpen(id)` when you need reactive UI updates:
199
+ Optionally sync active modals with the browser URL (hash or query) and browser Back button:
189
200
 
190
201
  ```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
- }
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 | 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. |
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
- ## Architecture
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
- - **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.
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