@okyrychenko-dev/react-modal-manager 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +143 -49
- package/dist/index.cjs +234 -275
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -6
- package/dist/index.d.ts +8 -6
- package/dist/index.js +235 -276
- package/dist/index.js.map +1 -1
- package/package.json +43 -28
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Allow modal definitions whose input is `void` or `undefined` to be opened without an input argument through the modal manager, registered definitions, and typed registries.
|
|
13
|
+
- Add verified React 18 and React 19 compatibility, including Strict Mode and independent-root coverage.
|
|
14
|
+
- Add tested SSR, hydration, and Next.js App Router/React Server Components guidance.
|
|
15
|
+
- Add compile-checked adoption examples, packed-package contract checks, lifecycle benchmarks, and reproducible competitive package checks.
|
|
16
|
+
- Add automated pnpm-based CI and provenance-enabled npm release workflows.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Move modal lifecycle ownership entirely into each `ModalProvider`, with React observing the lifecycle state directly.
|
|
21
|
+
- Simplify imperative modal access: pass a typed registry directly to `ModalProvider`; external registry calls route to the most recently mounted matching provider and fall back when it unmounts.
|
|
22
|
+
- Remove Zustand from the public installation contract. React is now the only peer dependency; `@okyrychenko-dev/type-utils` is the sole runtime dependency.
|
|
23
|
+
- Migrate repository development, validation, package inspection, and release workflows from npm to pnpm.
|
|
24
|
+
- Expand the README with provider-scope, lifecycle, registry, SSR/RSC, custom-renderer, accessibility, adoption, and troubleshooting guidance.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Preserve provider lifecycle state during React Strict Mode effect replay.
|
|
29
|
+
- Preserve registry routing across nested providers, adjacent providers, and independent React roots.
|
|
30
|
+
- Make lifecycle Storybook examples repeatable after modal settlement.
|
|
31
|
+
|
|
32
|
+
[0.2.0]: https://github.com/okyrychenko-dev/react-modal-manager/compare/v0.1.0...v0.2.0
|
package/README.md
CHANGED
|
@@ -35,42 +35,51 @@ const app = (
|
|
|
35
35
|
|
|
36
36
|
## Why This Library
|
|
37
37
|
|
|
38
|
-
- **Typed results, not `any`.** `open<TInput, TResult>(def, input)` returns a `Promise<TResult>`. Both sides of the call are checked.
|
|
39
|
-
- **Per-provider isolation.** Each `ModalProvider` owns
|
|
40
|
-
- **Open from non-React code.** A typed registry
|
|
41
|
-
- **UI-agnostic core.** A single `renderer`
|
|
38
|
+
- **Typed results, not `any`.** `open<TInput, TResult>(def, input)` returns a `Promise<TResult>`. Both sides of the call are checked, and inputless modals can omit the argument.
|
|
39
|
+
- **Per-provider isolation.** Each `ModalProvider` owns an independent lifecycle whose authoritative state React observes directly — no global lifecycle singleton, so subtrees and tests never leak modal state into each other.
|
|
40
|
+
- **Open from non-React code.** A typed registry lets event buses, command palettes, and action maps open modals while keeping full inference.
|
|
41
|
+
- **UI-agnostic core.** A single `renderer` seam lets you plug in portals, overlays, animations, or any design system. The core never prescribes DOM or styling.
|
|
42
42
|
- **Built-in `confirm()`** with a typed, discriminated-union result — useful from day one, replaceable when you need your own design.
|
|
43
43
|
- **Promise-shaped lifecycle.** Dismissals reject with `ModalDismissError`; exit animations are supported through `closeDelayMs` + an `"open" | "closing"` status.
|
|
44
44
|
|
|
45
|
-
### Compared to `nice-modal-react
|
|
45
|
+
### Compared to [`@ebay/nice-modal-react`](https://github.com/eBay/nice-modal-react)
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
| --- | --- | --- |
|
|
49
|
-
| Result typing | `Promise<TResult>`, fully inferred | result is effectively `unknown` / `any` |
|
|
50
|
-
| State scope | isolated per `ModalProvider` | single global singleton |
|
|
51
|
-
| Open from anywhere | typed registry / controller (LIFO provider stack) | global `NiceModal.show(id)` |
|
|
52
|
-
| Built-in confirm | typed `ConfirmModalResult` | none |
|
|
53
|
-
| UI coupling | UI-agnostic `renderer` boundary | you render it yourself |
|
|
54
|
-
| Concepts to first modal | 1 (`confirm`) — or define → register → open for custom modals | 1 (`show`) |
|
|
47
|
+
Revalidated **2026-09-13** for this package's **0.2.0** release line (through `3a98aa8`) and the current stable [`@ebay/nice-modal-react` 1.2.13](https://www.npmjs.com/package/@ebay/nice-modal-react/v/1.2.13). “Verified behavior” below means an executable public-surface check; “architecture” describes source structure and is not itself a consumer guarantee.
|
|
55
48
|
|
|
56
|
-
|
|
49
|
+
| Area | `react-modal-manager` | `nice-modal-react` | Evidence kind |
|
|
50
|
+
| --- | --- | --- | --- |
|
|
51
|
+
| Input and result typing | A definition or registry key couples input to `Promise<TResult>` | Component props are inferred; `show()` result is caller-selected and handlers expose `Promise<unknown>` | Verified declarations: [local packed-consumer fixture](scripts/package-consumer.typecheck.ts), [Nice Modal 1.2.13 declarations](https://unpkg.com/@ebay/nice-modal-react@1.2.13/lib/esm/index.d.ts) |
|
|
52
|
+
| Lifecycle correctness | Per-instance handles; resolve, reject, and dismiss settle once; optional closing phase precedes removal | Promise-based `show`; separate `hide` and `remove`; UI-library helpers connect removal to exit callbacks | Verified local behavior: [lifecycle tests](src/lifecycle/__tests__/modalLifecycle.test.ts); documented competitor behavior: [Nice Modal usage and helpers](https://github.com/eBay/nice-modal-react/tree/1.2.13#usage) |
|
|
53
|
+
| Provider isolation | Nested, adjacent, and independent roots own isolated lifecycle state | Each provider creates reducer state, while imperative dispatch, registrations, and promise callbacks are module-level | Verified local behavior: [root tests](src/provider/__tests__/ModalProvider.roots.test.tsx); competitor architecture: [1.2.13 source](https://github.com/eBay/nice-modal-react/blob/1.2.13/src/index.tsx) |
|
|
54
|
+
| React compatibility | Declares React 18 and 19 and runs the same root/Strict Mode suite against both | Declares React and React DOM `>16.8.0`; the published package was developed with React 17 | Verified local matrix: [CI](.github/workflows/ci.yml); published competitor metadata: [package manifest](https://unpkg.com/@ebay/nice-modal-react@1.2.13/package.json) |
|
|
55
|
+
| SSR and RSC | Provider server rendering, separate-request isolation, and hydration are tested; RSC usage has a compiled Client Component example | No SSR, hydration, or React Server Components contract is documented in the 1.2.13 README | Verified local behavior: [SSR tests](src/provider/__tests__/ModalProvider.ssr.test.tsx) and [RSC-style fixture](examples/next-app-router-provider.typecheck.tsx); competitor documentation: [1.2.13 README](https://github.com/eBay/nice-modal-react/tree/1.2.13) |
|
|
56
|
+
| Imperative access | A typed registry opens outside React after being bound to a provider; keys, input, and results are inferred | `NiceModal.show(component, props)` or a registered string id can be called directly after a provider establishes the module-level dispatch | Verified local behavior: [registry tests](src/registry/__tests__/createModalRegistry.test.tsx); documented competitor behavior: [component and id APIs](https://github.com/eBay/nice-modal-react/tree/1.2.13#using-your-modal-component) |
|
|
57
|
+
| Rendering independence | Lifecycle renders modal definitions through a replaceable wrapper; no portal, overlay, CSS, or design-system dependency | Supplies no dialog markup and wraps consumer components; includes helpers for Ant Design, MUI, and React Bootstrap lifecycles | Public interfaces: [local renderer types](src/types/modal.ts), [Nice Modal helpers](https://unpkg.com/@ebay/nice-modal-react@1.2.13/lib/esm/index.d.ts) |
|
|
58
|
+
| Accessibility composition | Built-in confirmation provides dialog semantics, initial focus, focus trapping, and safe destructive focus; custom modals/renderers remain consumer-owned | Accessibility belongs entirely to the consumer’s chosen modal component or UI library | Verified local behavior: [confirmation tests](src/confirm/__tests__/ConfirmModal.test.tsx); documented competitor scope: [“not a React modal component”](https://github.com/eBay/nice-modal-react/tree/1.2.13#nice-modal) |
|
|
59
|
+
| First-use ergonomics | `confirm()` is the shortest path; custom flows define a modal and open it directly or through a registry | `show(component, props)` is the shortest path; string access adds `register(id, component)` | Documented public APIs: [this README](#quick-start), [Nice Modal usage](https://github.com/eBay/nice-modal-react/tree/1.2.13#usage) |
|
|
60
|
+
| Package cost | Recorded minimal `createModal` consumer: **2,058 B / 1,044 B gzip**; React is the only peer and `type-utils` the only runtime dependency | Reproduced minimal named-`show` consumer: **758 B / 473 B gzip**; zero runtime dependencies, with React and React DOM as peers | Reproduce with [`package:check`](scripts/check-packed-package.mjs) and [`competitive:check`](scripts/check-competitive-package.mjs). The entry points differ, so these are package-cost observations, not a universal size ranking. |
|
|
61
|
+
| Performance | Optimized-build raw samples and summaries cover mount, unmount, open/render, settlement, delayed removal, stacking, and registry routing | No like-for-like run was made against the competitor | Reproducible local evidence: [`benchmark:lifecycle`](scripts/benchmark-lifecycle.mjs). No performance winner is claimed. |
|
|
62
|
+
| Maintenance status | 0.2.0 is the release line evaluated in this repository | 1.2.13 was published 2023-10-03; it remains the npm `latest` release on the evaluation date | Release evidence: [local manifest](package.json), [npm version](https://www.npmjs.com/package/@ebay/nice-modal-react/v/1.2.13), [GitHub release](https://github.com/eBay/nice-modal-react/releases/tag/1.2.13) |
|
|
63
|
+
|
|
64
|
+
The main trade-off is deliberate: this package does not provide unchecked `show("any-string")` routing. Imperative callers import a typed definition or use a typed registry, and a registry must be bound to a mounted provider. Nice Modal’s global component/id calls require less setup and can be more convenient when that trade-off is acceptable. Conversely, this package’s provider ownership, result inference, SSR behavior, and built-in confirmation are explicit tested contracts rather than conclusions drawn only from implementation structure.
|
|
57
65
|
|
|
58
66
|
## Installation
|
|
59
67
|
|
|
60
68
|
```bash
|
|
61
|
-
npm install @okyrychenko-dev/react-modal-manager
|
|
69
|
+
npm install @okyrychenko-dev/react-modal-manager
|
|
62
70
|
# or
|
|
63
|
-
yarn add @okyrychenko-dev/react-modal-manager
|
|
71
|
+
yarn add @okyrychenko-dev/react-modal-manager
|
|
64
72
|
# or
|
|
65
|
-
pnpm add @okyrychenko-dev/react-modal-manager
|
|
73
|
+
pnpm add @okyrychenko-dev/react-modal-manager
|
|
66
74
|
```
|
|
67
75
|
|
|
68
76
|
Peer dependencies:
|
|
69
77
|
|
|
70
78
|
- [React](https://react.dev/) `^18.0.0 || ^19.0.0`
|
|
71
|
-
- [Zustand](https://zustand-demo.pmnd.rs/) `^5.0.0`
|
|
72
79
|
|
|
73
|
-
|
|
80
|
+
Modal lifecycle state is provider-owned and observed directly by React. There is no global lifecycle singleton.
|
|
81
|
+
|
|
82
|
+
The package also installs its small runtime guard dependency automatically; React is the only peer dependency consumers provide.
|
|
74
83
|
|
|
75
84
|
## Quick Start
|
|
76
85
|
|
|
@@ -110,6 +119,20 @@ function ReportsPage() {
|
|
|
110
119
|
}
|
|
111
120
|
```
|
|
112
121
|
|
|
122
|
+
## Adoption Path
|
|
123
|
+
|
|
124
|
+
Start with the smallest API that solves the current problem, then add the next capability only when the application needs it:
|
|
125
|
+
|
|
126
|
+
1. Use [`confirm()`](#confirmation-modals) for a typed yes/no decision.
|
|
127
|
+
2. Define a [typed custom modal](#typed-modal-flow) when the flow needs application-specific input, UI, or results.
|
|
128
|
+
3. Keep the returned [modal handle](#typed-modal-flow) when the caller must identify or dismiss that exact instance.
|
|
129
|
+
4. Add a [typed registry](#typed-modal-registry) for commands that originate outside React.
|
|
130
|
+
5. Supply a [custom renderer](#custom-renderer) for overlays, portals, design-system shells, and exit animations.
|
|
131
|
+
6. Split independent application areas into [provider scopes](#provider-scope).
|
|
132
|
+
7. Follow the [Next.js App Router guidance](#nextjs-app-router-ssr) for SSR and React Server Components.
|
|
133
|
+
|
|
134
|
+
The complete path is compile-checked as a package consumer in [`examples/adoption-paths.typecheck.tsx`](examples/adoption-paths.typecheck.tsx). The client boundary used by a server layout is checked separately in [`examples/next-app-router-provider.typecheck.tsx`](examples/next-app-router-provider.typecheck.tsx). Interactive equivalents live in Storybook under `Components/confirmModal`, `Context/ModalProvider`, and `Components/ModalViewport`.
|
|
135
|
+
|
|
113
136
|
## Type Safety
|
|
114
137
|
|
|
115
138
|
This is where the library earns its place. Define a modal once and every call site is checked end to end.
|
|
@@ -122,9 +145,16 @@ interface RenameInput {
|
|
|
122
145
|
currentName: string;
|
|
123
146
|
}
|
|
124
147
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
148
|
+
interface RenameSucceededResult {
|
|
149
|
+
status: "renamed";
|
|
150
|
+
name: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface RenameCancelledResult {
|
|
154
|
+
status: "cancelled";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type RenameResult = RenameSucceededResult | RenameCancelledResult;
|
|
128
158
|
|
|
129
159
|
function RenameModal({ close, input }: ModalComponentProps<RenameInput, RenameResult>) {
|
|
130
160
|
// `input` is RenameInput. `close` only accepts a RenameResult.
|
|
@@ -221,6 +251,16 @@ const result = await handle;
|
|
|
221
251
|
|
|
222
252
|
The handle's `dismiss()` stays bound to the provider that opened the modal.
|
|
223
253
|
|
|
254
|
+
For a modal that needs no input, declare its input as `void` and omit the second argument:
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
const infoModal = createModal<void, void>({ component: InfoModal });
|
|
258
|
+
|
|
259
|
+
await modal.open(infoModal);
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Modals with any other input type still require an input argument.
|
|
263
|
+
|
|
224
264
|
## Typed Modal Registry
|
|
225
265
|
|
|
226
266
|
Use `createModalRegistry()` when code needs to open modals by a stable key while keeping typed input and result contracts. This suits command palettes, event buses, action maps, and configuration-driven flows. Pass the registry straight to `ModalProvider` — there is no controller to wire up.
|
|
@@ -262,6 +302,32 @@ export async function renameFromAction(reportId: string, currentName: string) {
|
|
|
262
302
|
|
|
263
303
|
The registry key is type-checked, and TypeScript infers the required input and the returned result from the modal registered under that key. `modals.open` from outside the React tree targets the most recently mounted `ModalProvider` bound to that registry (providers form a LIFO stack and fall back on unmount).
|
|
264
304
|
|
|
305
|
+
Before a provider binds the registry, `modals.isReady()` is `false` and registry operations throw. Binding happens in a client effect, so a registry is intentionally unbound during server rendering.
|
|
306
|
+
|
|
307
|
+
## Provider Scope
|
|
308
|
+
|
|
309
|
+
Each `ModalProvider` owns an independent lifecycle. `useModalManager()` always targets the nearest provider, so adjacent or nested application areas can keep their modal state, renderers, and teardown behavior isolated. A handle remains bound to the provider that created it, even when another provider opens the same modal definition.
|
|
310
|
+
|
|
311
|
+
Use a separate registry for each strictly isolated scope. Binding the same registry to multiple providers is a deliberate routing mechanism instead: external calls target the most recently mounted binding and fall back to the previous binding when it unmounts.
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
const accountModals = createModalRegistry({ rename: renameReportModal });
|
|
315
|
+
const workspaceModals = createModalRegistry({ rename: renameReportModal });
|
|
316
|
+
|
|
317
|
+
function App() {
|
|
318
|
+
return (
|
|
319
|
+
<>
|
|
320
|
+
<ModalProvider registry={accountModals} renderer={AccountModalRenderer}>
|
|
321
|
+
<AccountSettings />
|
|
322
|
+
</ModalProvider>
|
|
323
|
+
<ModalProvider registry={workspaceModals} renderer={WorkspaceModalRenderer}>
|
|
324
|
+
<Workspace />
|
|
325
|
+
</ModalProvider>
|
|
326
|
+
</>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
265
331
|
## Confirmation Modals
|
|
266
332
|
|
|
267
333
|
`modal.confirm()` (and `registry.confirm()`) opens the built-in confirmation modal and resolves to a typed, discriminated-union result.
|
|
@@ -283,7 +349,7 @@ if (result.confirmed) {
|
|
|
283
349
|
The bundled `confirmModal` is an **accessible, unstyled reference implementation**:
|
|
284
350
|
|
|
285
351
|
- `role="dialog"` with `aria-modal="true"`, `aria-labelledby` (title) and `aria-describedby` (description)
|
|
286
|
-
- the confirm button receives focus on open (the cancel button for `variant: "danger"`, so a stray Enter never confirms a destructive action), and focus returns to the
|
|
352
|
+
- the confirm button receives focus on open (the cancel button for `variant: "danger"`, so a stray Enter never confirms a destructive action), and focus returns to the previously focused element when the modal is removed
|
|
287
353
|
- `Tab` / `Shift+Tab` are trapped within the dialog
|
|
288
354
|
- `Escape` dismisses it (unless `dismissible: false`)
|
|
289
355
|
|
|
@@ -313,13 +379,13 @@ function App() {
|
|
|
313
379
|
}
|
|
314
380
|
```
|
|
315
381
|
|
|
316
|
-
The core prescribes no DOM structure, focus management, or styling —
|
|
382
|
+
The core prescribes no DOM structure, focus management, or styling — renderers provide those while reusing the same modal manager interface. When `closeDelayMs` is greater than `0`, resolved, dismissed, or rejected instances move from `modal.status === "open"` to `modal.status === "closing"` before removal, giving exit animations time to run. A value of `0` or less removes the instance immediately. Updating the prop changes the removal delay used by later settlements in that provider.
|
|
317
383
|
|
|
318
384
|
## Recipes
|
|
319
385
|
|
|
320
386
|
### Next.js App Router (SSR)
|
|
321
387
|
|
|
322
|
-
The
|
|
388
|
+
The lifecycle is created lazily **per provider**, lives in React context, and exposes its state directly to React through external-store observation. It owns opening, settlement, dismissal, delayed removal, and disposal. Server rendering reads a stable empty snapshot, and there is no module-level singleton or shared state across requests. The provider uses hooks, so it must run in a Client Component. Wrap it once and render that wrapper from your server layout.
|
|
323
389
|
|
|
324
390
|
```tsx
|
|
325
391
|
// app/providers/modal-provider.tsx
|
|
@@ -350,6 +416,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
350
416
|
|
|
351
417
|
Modal components and any component calling `useModalManager()` must also be Client Components (`"use client"`).
|
|
352
418
|
|
|
419
|
+
Opening modal work while React is rendering on the server is unsupported. The server snapshot is intentionally empty: open modals from event handlers, effects, command handlers, or other client-side code after hydration. A registry likewise remains unbound until its provider's client effect runs, so check `registry.isReady()` before dispatching startup commands from outside React.
|
|
420
|
+
|
|
353
421
|
### Tailwind CSS
|
|
354
422
|
|
|
355
423
|
Provide the overlay and centering through the `renderer`, and style modal components with Tailwind utilities.
|
|
@@ -374,18 +442,31 @@ function TailwindRenderer({ children, modal }: ModalRendererProps) {
|
|
|
374
442
|
</ModalProvider>;
|
|
375
443
|
```
|
|
376
444
|
|
|
377
|
-
### shadcn/ui
|
|
445
|
+
### shadcn/ui (Radix)
|
|
378
446
|
|
|
379
|
-
Use
|
|
447
|
+
Use the current [Radix-based shadcn `Dialog`](https://ui.shadcn.com/docs/components/radix/dialog) as a controlled renderer shell, so every opened modal is wrapped in the design system's overlay and animations. Route `onOpenChange(false)` back to the modal manager so Escape, outside interaction, and shadcn's generated close button dismiss the correct instance.
|
|
380
448
|
|
|
381
449
|
```tsx
|
|
450
|
+
import {
|
|
451
|
+
type ModalRendererProps,
|
|
452
|
+
useModalManager,
|
|
453
|
+
} from "@okyrychenko-dev/react-modal-manager";
|
|
382
454
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
|
383
|
-
import type { ModalRendererProps } from "@okyrychenko-dev/react-modal-manager";
|
|
384
455
|
|
|
385
456
|
function ShadcnRenderer({ children, modal }: ModalRendererProps) {
|
|
386
|
-
|
|
457
|
+
const modalManager = useModalManager();
|
|
458
|
+
|
|
459
|
+
function handleOpenChange(open: boolean) {
|
|
460
|
+
if (!open && modal.status === "open") {
|
|
461
|
+
modalManager.dismiss(modal.instanceId);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
387
465
|
return (
|
|
388
|
-
<Dialog
|
|
466
|
+
<Dialog
|
|
467
|
+
open={modal.status === "open"}
|
|
468
|
+
onOpenChange={handleOpenChange}
|
|
469
|
+
>
|
|
389
470
|
<DialogContent>{children}</DialogContent>
|
|
390
471
|
</Dialog>
|
|
391
472
|
);
|
|
@@ -396,7 +477,11 @@ function ShadcnRenderer({ children, modal }: ModalRendererProps) {
|
|
|
396
477
|
</ModalProvider>;
|
|
397
478
|
```
|
|
398
479
|
|
|
399
|
-
|
|
480
|
+
This provider-wide renderer is for content-only custom modals. Each modal rendered inside `DialogContent` must compose shadcn's `DialogHeader`, `DialogTitle`, and, when useful, `DialogDescription`; otherwise Radix cannot establish the accessible title/description relationship.
|
|
481
|
+
|
|
482
|
+
Do not use the built-in confirmation modal with this renderer: it already owns its dialog semantics, so wrapping it in `DialogContent` would create nested dialogs and omit Radix's required `DialogTitle`. If this provider calls `confirm()`, pass a content-only custom `confirmModal` that composes `DialogHeader`, `DialogTitle`, and `DialogDescription` inside this existing shell. Do not nest another `Dialog` or `AlertDialog` in it.
|
|
483
|
+
|
|
484
|
+
Match `closeDelayMs` to your generated component's exit-animation duration. The example uses 200 ms, matching the current [Radix-based component's `duration-200`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/dialog.tsx).
|
|
400
485
|
|
|
401
486
|
### React Hook Form inside a modal
|
|
402
487
|
|
|
@@ -472,6 +557,7 @@ Type exports:
|
|
|
472
557
|
- `ModalId`
|
|
473
558
|
- `ModalInstanceId`
|
|
474
559
|
- `ModalInstanceStatus`
|
|
560
|
+
- `ModalOpenArgs`
|
|
475
561
|
- `ModalManager`
|
|
476
562
|
- `ModalOptions`
|
|
477
563
|
- `ModalProviderProps`
|
|
@@ -497,7 +583,7 @@ Creates an isolated modal manager for a React subtree and renders active modals.
|
|
|
497
583
|
- `renderer?: ModalRenderer` — Optional wrapper for each rendered modal instance
|
|
498
584
|
- `confirmModal?: ModalDefinition<ConfirmModalParams, ConfirmModalResult>` — Optional custom confirm modal implementation
|
|
499
585
|
- `registry?: ModalRegistry` — Optional typed modal registry bound to this provider while it is mounted
|
|
500
|
-
- `closeDelayMs?: number` — Delay before removing a closing
|
|
586
|
+
- `closeDelayMs?: number` — Delay before removing a closing lifecycle instance. Values of `0` or less remove immediately. Defaults to `0`; prop updates apply to later settlements
|
|
501
587
|
|
|
502
588
|
### `useModalManager()`
|
|
503
589
|
|
|
@@ -505,7 +591,7 @@ Returns the modal manager from the nearest `ModalProvider`.
|
|
|
505
591
|
|
|
506
592
|
**Returns:**
|
|
507
593
|
|
|
508
|
-
- `open(modal,
|
|
594
|
+
- `open(modal, ...args: ModalOpenArgs<TInput>): ModalHandle<TResult>`
|
|
509
595
|
- `confirm(params): Promise<ConfirmModalResult>`
|
|
510
596
|
- `dismiss(instanceId, reason?): void`
|
|
511
597
|
- `closeAll(reason?): void`
|
|
@@ -516,16 +602,18 @@ Creates a typed modal definition.
|
|
|
516
602
|
|
|
517
603
|
**Options:**
|
|
518
604
|
|
|
519
|
-
- `id?: string` — Optional stable modal definition id.
|
|
605
|
+
- `id?: string` — Optional stable modal definition id. A unique definition id is generated when omitted
|
|
520
606
|
- `component: ModalComponent<TInput, TResult>` — React component that receives typed input and completion callbacks
|
|
521
607
|
|
|
608
|
+
For a modal declared with `TInput = void` (or `undefined`), omit the input argument: `modal.open(infoModal)`. Modals with any other input type still require it.
|
|
609
|
+
|
|
522
610
|
### `createModalRegistry(definitions)`
|
|
523
611
|
|
|
524
612
|
Creates a typed registry for opening modals by key. Bind it directly with `<ModalProvider registry={registry}>`.
|
|
525
613
|
|
|
526
614
|
**Returns:**
|
|
527
615
|
|
|
528
|
-
- `open(key,
|
|
616
|
+
- `open(key, ...args: ModalOpenArgs<TInput>): ModalHandle<TResult>` — input is optional only when the registered modal uses `void` or `undefined`
|
|
529
617
|
- `confirm(params): Promise<ConfirmModalResult>`
|
|
530
618
|
- `dismiss(instanceId, reason?): void`
|
|
531
619
|
- `closeAll(reason?): void`
|
|
@@ -535,17 +623,17 @@ Creates a typed registry for opening modals by key. Bind it directly with `<Moda
|
|
|
535
623
|
|
|
536
624
|
Props passed to custom modal components.
|
|
537
625
|
|
|
538
|
-
- `input: TInput` — Input supplied to `modal.open()`
|
|
626
|
+
- `input: ModalOpenArgs<TInput>[0]` — Input supplied to `modal.open()`; `undefined` when a `void` input is omitted
|
|
539
627
|
- `instanceId: string` — Runtime modal instance id
|
|
540
|
-
- `close(result: TResult): void` — Resolve the modal promise and
|
|
541
|
-
- `dismiss(reason?): void` — Reject with `ModalDismissError` and
|
|
542
|
-
- `reject(error): void` — Reject with an error and
|
|
628
|
+
- `close(result: TResult): void` — Resolve the modal promise and begin closing the instance
|
|
629
|
+
- `dismiss(reason?): void` — Reject with `ModalDismissError` and begin closing the instance
|
|
630
|
+
- `reject(error): void` — Reject with an error and begin closing the instance
|
|
543
631
|
|
|
544
|
-
When `closeDelayMs` is
|
|
632
|
+
When `closeDelayMs` is greater than `0`, `close`, `dismiss`, and `reject` settle the promise immediately, mark the modal as `"closing"`, and remove it after the delay. A value of `0` or less removes the instance immediately after settlement.
|
|
545
633
|
|
|
546
634
|
### `ModalRendererProps`
|
|
547
635
|
|
|
548
|
-
Props passed to the `renderer`
|
|
636
|
+
Props passed to the `renderer` seam.
|
|
549
637
|
|
|
550
638
|
- `children: ReactNode` — Rendered modal component
|
|
551
639
|
- `modal.definitionId: string` — Stable modal definition id
|
|
@@ -567,14 +655,16 @@ Opens the built-in (or provided) confirmation modal.
|
|
|
567
655
|
|
|
568
656
|
**Returns:**
|
|
569
657
|
|
|
658
|
+
`ConfirmModalResult` is a discriminated union. Its structure is shown with named branches below so the successful and cancelled paths remain explicit; only `ConfirmModalResult` is exported from the package root.
|
|
659
|
+
|
|
570
660
|
```ts
|
|
571
|
-
|
|
661
|
+
type ConfirmationModalRejectReason = "cancel" | "dismiss";
|
|
572
662
|
|
|
573
|
-
|
|
663
|
+
interface ConfirmationModalConfirmedResult {
|
|
574
664
|
confirmed: true;
|
|
575
665
|
}
|
|
576
666
|
|
|
577
|
-
|
|
667
|
+
interface ConfirmationModalRejectedResult {
|
|
578
668
|
confirmed: false;
|
|
579
669
|
reason: ConfirmationModalRejectReason;
|
|
580
670
|
}
|
|
@@ -613,11 +703,15 @@ react-action-guard-dialog
|
|
|
613
703
|
## Development
|
|
614
704
|
|
|
615
705
|
```bash
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
706
|
+
pnpm install --frozen-lockfile
|
|
707
|
+
pnpm run typecheck
|
|
708
|
+
pnpm run lint
|
|
709
|
+
pnpm run format:check
|
|
710
|
+
pnpm run test:run
|
|
711
|
+
pnpm run test:coverage
|
|
712
|
+
pnpm run build
|
|
713
|
+
pnpm run build-storybook
|
|
714
|
+
pnpm pack --pack-destination /tmp/react-modal-manager-pack
|
|
621
715
|
```
|
|
622
716
|
|
|
623
717
|
## License
|