@lerx/promise-modal 0.10.4 → 0.11.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/docs/claude/commands/guide.md +760 -0
- package/docs/claude/skills/expert/SKILL.md +171 -0
- package/docs/claude/skills/expert/knowledge/advanced-patterns.md +294 -0
- package/docs/claude/skills/expert/knowledge/api-reference.md +175 -0
- package/docs/claude/skills/expert/knowledge/hooks-reference.md +207 -0
- package/docs/claude/skills/expert/knowledge/type-definitions.md +172 -0
- package/docs/en/SPECIFICATION.md +1185 -0
- package/docs/ko/SPECIFICATION.md +1193 -0
- package/package.json +5 -4
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
# @lerx/promise-modal Specification
|
|
2
|
+
|
|
3
|
+
> Universal React Modal Utility with Promise-based API
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`@lerx/promise-modal` is a React-based universal modal utility that provides:
|
|
8
|
+
|
|
9
|
+
- **Promise-based interactions**: Alert, confirm, and prompt modals that return promises
|
|
10
|
+
- **Universal usage**: Can be used both inside and outside React components
|
|
11
|
+
- **High customizability**: Every component can be customized
|
|
12
|
+
- **Automatic lifecycle management**: Handles mount/unmount and animations
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
1. [Installation](#installation)
|
|
19
|
+
2. [Quick Start](#quick-start)
|
|
20
|
+
3. [Architecture](#architecture)
|
|
21
|
+
4. [Core API](#core-api)
|
|
22
|
+
- [alert](#alert)
|
|
23
|
+
- [confirm](#confirm)
|
|
24
|
+
- [prompt](#prompt)
|
|
25
|
+
5. [Hooks](#hooks)
|
|
26
|
+
- [useModal](#usemodal)
|
|
27
|
+
- [useActiveModalCount](#useactivemodalcount)
|
|
28
|
+
- [useModalAnimation](#usemodalanimation)
|
|
29
|
+
- [useModalDuration](#usemodalduration)
|
|
30
|
+
- [useDestroyAfter](#usedestroyafter)
|
|
31
|
+
- [useSubscribeModal](#usesubscribemodal)
|
|
32
|
+
- [useInitializeModal](#useinitializemodal)
|
|
33
|
+
- [useModalOptions](#usemodaloptions)
|
|
34
|
+
- [useModalBackdrop](#usemodalbackdrop)
|
|
35
|
+
6. [Components](#components)
|
|
36
|
+
- [ModalProvider](#modalprovider)
|
|
37
|
+
- [Custom Components](#custom-components)
|
|
38
|
+
7. [Type Definitions](#type-definitions)
|
|
39
|
+
8. [Usage Patterns](#usage-patterns)
|
|
40
|
+
9. [Advanced Examples](#advanced-examples)
|
|
41
|
+
10. [AbortSignal Support](#abortsignal-support)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Using yarn
|
|
49
|
+
yarn add @lerx/promise-modal
|
|
50
|
+
|
|
51
|
+
# Using npm
|
|
52
|
+
npm install @lerx/promise-modal
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Peer Dependencies
|
|
56
|
+
|
|
57
|
+
- React 18-19
|
|
58
|
+
- React DOM 18-19
|
|
59
|
+
|
|
60
|
+
### Compatibility
|
|
61
|
+
|
|
62
|
+
- Node.js 16.11.0 or later
|
|
63
|
+
- Modern browsers (Chrome 94+, Firefox 93+, Safari 15+)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
### 1. Setup Provider
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { ModalProvider } from '@lerx/promise-modal';
|
|
73
|
+
|
|
74
|
+
function App() {
|
|
75
|
+
return (
|
|
76
|
+
<ModalProvider>
|
|
77
|
+
<YourApp />
|
|
78
|
+
</ModalProvider>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. Use Modal Functions
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { alert, confirm, prompt } from '@lerx/promise-modal';
|
|
87
|
+
|
|
88
|
+
// Alert
|
|
89
|
+
await alert({
|
|
90
|
+
title: 'Notice',
|
|
91
|
+
content: 'Operation completed.',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Confirm
|
|
95
|
+
const result = await confirm({
|
|
96
|
+
title: 'Confirm',
|
|
97
|
+
content: 'Are you sure?',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Prompt
|
|
101
|
+
const name = await prompt<string>({
|
|
102
|
+
title: 'Enter Name',
|
|
103
|
+
defaultValue: '',
|
|
104
|
+
Input: ({ value, onChange }) => (
|
|
105
|
+
<input value={value} onChange={(e) => onChange(e.target.value)} />
|
|
106
|
+
),
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Architecture
|
|
113
|
+
|
|
114
|
+
### Layer Structure
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
118
|
+
│ Your Application │
|
|
119
|
+
├─────────────────────────────────────────────────────────────┤
|
|
120
|
+
│ Core API Layer │
|
|
121
|
+
│ ├── alert() │
|
|
122
|
+
│ ├── confirm() │
|
|
123
|
+
│ └── prompt() │
|
|
124
|
+
├─────────────────────────────────────────────────────────────┤
|
|
125
|
+
│ Application Layer │
|
|
126
|
+
│ └── ModalManager (Singleton) │
|
|
127
|
+
│ ├── DOM anchoring │
|
|
128
|
+
│ ├── Style injection │
|
|
129
|
+
│ └── Modal lifecycle │
|
|
130
|
+
├─────────────────────────────────────────────────────────────┤
|
|
131
|
+
│ Bootstrap Layer │
|
|
132
|
+
│ └── ModalProvider │
|
|
133
|
+
│ ├── Initialization │
|
|
134
|
+
│ └── Component setup │
|
|
135
|
+
├─────────────────────────────────────────────────────────────┤
|
|
136
|
+
│ Provider Layer │
|
|
137
|
+
│ ├── ModalManagerContext │
|
|
138
|
+
│ ├── ConfigurationContext │
|
|
139
|
+
│ └── UserDefinedContext │
|
|
140
|
+
├─────────────────────────────────────────────────────────────┤
|
|
141
|
+
│ Component Layer │
|
|
142
|
+
│ ├── Anchor │
|
|
143
|
+
│ ├── Background │
|
|
144
|
+
│ ├── Foreground │
|
|
145
|
+
│ └── Fallback Components │
|
|
146
|
+
└─────────────────────────────────────────────────────────────┘
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Design Patterns
|
|
150
|
+
|
|
151
|
+
| Pattern | Usage |
|
|
152
|
+
|---------|-------|
|
|
153
|
+
| **Promise-based API** | Modal functions return promises |
|
|
154
|
+
| **Provider Pattern** | Context providers for configuration |
|
|
155
|
+
| **Factory Pattern** | Node factory for modal types |
|
|
156
|
+
| **Observer Pattern** | Subscription system for state |
|
|
157
|
+
| **Singleton Pattern** | ModalManager for global state |
|
|
158
|
+
|
|
159
|
+
### Modal Node System
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
AbstractNode (Base Class)
|
|
163
|
+
├── AlertNode → Simple notifications
|
|
164
|
+
├── ConfirmNode → Yes/no confirmations
|
|
165
|
+
└── PromptNode → Input collection
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Each node provides:
|
|
169
|
+
- Subscription-based state management
|
|
170
|
+
- Promise resolution handling
|
|
171
|
+
- Lifecycle management
|
|
172
|
+
|
|
173
|
+
### Configuration Priority
|
|
174
|
+
|
|
175
|
+
Configuration is applied hierarchically, with lower levels overriding higher levels:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
Provider Settings (lowest) < Hook Settings < Handler Settings (highest)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
| Level | Location | Description |
|
|
182
|
+
|-------|----------|-------------|
|
|
183
|
+
| **Provider** | `ModalProvider` props | App-wide default settings |
|
|
184
|
+
| **Hook** | `useModal(config)` | Component-level settings |
|
|
185
|
+
| **Handler** | `alert/confirm/prompt(options)` | Per-modal settings |
|
|
186
|
+
|
|
187
|
+
#### Example
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// Provider level: global defaults
|
|
191
|
+
<ModalProvider options={{ duration: '500ms', closeOnBackdropClick: true }}>
|
|
192
|
+
<App />
|
|
193
|
+
</ModalProvider>
|
|
194
|
+
|
|
195
|
+
// Hook level: component defaults (overrides Provider settings)
|
|
196
|
+
const modal = useModal({
|
|
197
|
+
ForegroundComponent: CustomForeground,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Handler level: individual modal (overrides Hook settings)
|
|
201
|
+
modal.alert({
|
|
202
|
+
title: 'Notice',
|
|
203
|
+
duration: 200, // 500ms → 200ms override
|
|
204
|
+
ForegroundComponent: SpecialForeground, // CustomForeground override
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Core API
|
|
211
|
+
|
|
212
|
+
### alert
|
|
213
|
+
|
|
214
|
+
Opens a simple notification modal.
|
|
215
|
+
|
|
216
|
+
#### Signature
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
function alert<B = any>(options: AlertProps<B>): Promise<void>;
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### Parameters
|
|
223
|
+
|
|
224
|
+
| Option | Type | Default | Description |
|
|
225
|
+
|--------|------|---------|-------------|
|
|
226
|
+
| `title` | `ReactNode` | - | Modal title |
|
|
227
|
+
| `subtitle` | `ReactNode` | - | Subtitle below title |
|
|
228
|
+
| `content` | `ReactNode \| ComponentType<AlertContentProps>` | - | Modal content |
|
|
229
|
+
| `subtype` | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` | Modal type |
|
|
230
|
+
| `dimmed` | `boolean` | `true` | Dim background |
|
|
231
|
+
| `closeOnBackdropClick` | `boolean` | `true` | Close on backdrop |
|
|
232
|
+
| `manualDestroy` | `boolean` | `false` | Manual destroy mode |
|
|
233
|
+
| `duration` | `number \| string` | - | Animation duration (Handler level override) |
|
|
234
|
+
| `background` | `ModalBackground<B>` | - | Background settings |
|
|
235
|
+
| `footer` | `AlertFooterRender \| FooterOptions \| false` | - | Footer config |
|
|
236
|
+
| `ForegroundComponent` | `ComponentType<ModalFrameProps>` | - | Custom foreground |
|
|
237
|
+
| `BackgroundComponent` | `ComponentType<ModalFrameProps>` | - | Custom background |
|
|
238
|
+
| `signal` | `AbortSignal` | - | AbortSignal for canceling modal |
|
|
239
|
+
|
|
240
|
+
#### Example
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
await alert({
|
|
244
|
+
title: 'Success',
|
|
245
|
+
content: 'Your changes have been saved.',
|
|
246
|
+
subtype: 'success',
|
|
247
|
+
footer: { confirm: 'OK' },
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### confirm
|
|
254
|
+
|
|
255
|
+
Opens a confirmation modal for user decisions.
|
|
256
|
+
|
|
257
|
+
#### Signature
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
function confirm<B = any>(options: ConfirmProps<B>): Promise<boolean>;
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### Parameters
|
|
264
|
+
|
|
265
|
+
All options from `alert`, plus:
|
|
266
|
+
|
|
267
|
+
| Option | Type | Default | Description |
|
|
268
|
+
|--------|------|---------|-------------|
|
|
269
|
+
| `footer` | `ConfirmFooterRender \| FooterOptions \| false` | - | Footer config |
|
|
270
|
+
|
|
271
|
+
#### FooterOptions for confirm
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
interface FooterOptions {
|
|
275
|
+
confirm?: string;
|
|
276
|
+
cancel?: string;
|
|
277
|
+
hideConfirm?: boolean;
|
|
278
|
+
hideCancel?: boolean;
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### Returns
|
|
283
|
+
|
|
284
|
+
- `true` - User clicked confirm button
|
|
285
|
+
- `false` - User clicked cancel or backdrop
|
|
286
|
+
|
|
287
|
+
#### Example
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const shouldDelete = await confirm({
|
|
291
|
+
title: 'Delete Item',
|
|
292
|
+
content: 'This action cannot be undone.',
|
|
293
|
+
subtype: 'warning',
|
|
294
|
+
footer: {
|
|
295
|
+
confirm: 'Delete',
|
|
296
|
+
cancel: 'Keep',
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (shouldDelete) {
|
|
301
|
+
await deleteItem();
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### prompt
|
|
308
|
+
|
|
309
|
+
Opens a prompt modal to collect user input.
|
|
310
|
+
|
|
311
|
+
#### Signature
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
function prompt<T, B = any>(options: PromptProps<T, B>): Promise<T>;
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### Parameters
|
|
318
|
+
|
|
319
|
+
All options from `alert`, plus:
|
|
320
|
+
|
|
321
|
+
| Option | Type | Required | Description |
|
|
322
|
+
|--------|------|----------|-------------|
|
|
323
|
+
| `Input` | `(props: PromptInputProps<T>) => ReactNode` | Yes | Input component |
|
|
324
|
+
| `defaultValue` | `T` | No | Default value |
|
|
325
|
+
| `disabled` | `(value: T) => boolean` | No | Disable confirm |
|
|
326
|
+
| `returnOnCancel` | `boolean` | No | Return default on cancel |
|
|
327
|
+
|
|
328
|
+
#### PromptInputProps
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
interface PromptInputProps<T> {
|
|
332
|
+
value?: T;
|
|
333
|
+
defaultValue?: T;
|
|
334
|
+
onChange: (value: T | undefined) => void;
|
|
335
|
+
onConfirm: () => void;
|
|
336
|
+
onCancel: () => void;
|
|
337
|
+
context: any;
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
#### Example
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// Simple input
|
|
345
|
+
const email = await prompt<string>({
|
|
346
|
+
title: 'Enter Email',
|
|
347
|
+
defaultValue: '',
|
|
348
|
+
Input: ({ value, onChange }) => (
|
|
349
|
+
<input
|
|
350
|
+
type="email"
|
|
351
|
+
value={value}
|
|
352
|
+
onChange={(e) => onChange(e.target.value)}
|
|
353
|
+
/>
|
|
354
|
+
),
|
|
355
|
+
disabled: (value) => !value?.includes('@'),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Complex object
|
|
359
|
+
interface UserData {
|
|
360
|
+
name: string;
|
|
361
|
+
age: number;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const userData = await prompt<UserData>({
|
|
365
|
+
title: 'User Info',
|
|
366
|
+
defaultValue: { name: '', age: 0 },
|
|
367
|
+
Input: ({ value, onChange }) => (
|
|
368
|
+
<form>
|
|
369
|
+
<input
|
|
370
|
+
value={value.name}
|
|
371
|
+
onChange={(e) => onChange({ ...value, name: e.target.value })}
|
|
372
|
+
/>
|
|
373
|
+
<input
|
|
374
|
+
type="number"
|
|
375
|
+
value={value.age}
|
|
376
|
+
onChange={(e) => onChange({ ...value, age: Number(e.target.value) })}
|
|
377
|
+
/>
|
|
378
|
+
</form>
|
|
379
|
+
),
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Hooks
|
|
386
|
+
|
|
387
|
+
### useModal
|
|
388
|
+
|
|
389
|
+
Returns modal handlers tied to component lifecycle.
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
function useModal(config?: Partial<ModalOptions>): {
|
|
393
|
+
alert: typeof alert;
|
|
394
|
+
confirm: typeof confirm;
|
|
395
|
+
prompt: typeof prompt;
|
|
396
|
+
};
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
#### Key Feature
|
|
400
|
+
|
|
401
|
+
Modals automatically cleanup when the component unmounts.
|
|
402
|
+
|
|
403
|
+
#### Comparison
|
|
404
|
+
|
|
405
|
+
| Feature | Static Functions | useModal Hook |
|
|
406
|
+
|---------|-----------------|---------------|
|
|
407
|
+
| Lifecycle | Independent | Tied to component |
|
|
408
|
+
| Cleanup | Manual | Automatic |
|
|
409
|
+
| Usage | Anywhere | Inside components |
|
|
410
|
+
|
|
411
|
+
#### Example
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
function DeleteButton({ id }) {
|
|
415
|
+
const modal = useModal();
|
|
416
|
+
|
|
417
|
+
const handleDelete = async () => {
|
|
418
|
+
if (await modal.confirm({ content: 'Delete?' })) {
|
|
419
|
+
await deleteItem(id);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
return <button onClick={handleDelete}>Delete</button>;
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
### useActiveModalCount
|
|
430
|
+
|
|
431
|
+
Returns the count of active modals.
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
function useActiveModalCount(
|
|
435
|
+
validate?: (modal?: ModalNode) => boolean,
|
|
436
|
+
refreshKey?: string | number
|
|
437
|
+
): number;
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
#### Parameters
|
|
441
|
+
|
|
442
|
+
| Parameter | Type | Description |
|
|
443
|
+
|-----------|------|-------------|
|
|
444
|
+
| `validate` | `(modal) => boolean` | Filter function |
|
|
445
|
+
| `refreshKey` | `string \| number` | Force refresh key |
|
|
446
|
+
|
|
447
|
+
#### Example
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
function ModalCounter() {
|
|
451
|
+
const total = useActiveModalCount();
|
|
452
|
+
const alerts = useActiveModalCount((m) => m?.type === 'alert');
|
|
453
|
+
|
|
454
|
+
return <div>Total: {total}, Alerts: {alerts}</div>;
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
### useModalAnimation
|
|
461
|
+
|
|
462
|
+
Provides animation state callbacks.
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
function useModalAnimation(
|
|
466
|
+
visible: boolean,
|
|
467
|
+
options: {
|
|
468
|
+
onVisible?: () => void;
|
|
469
|
+
onHidden?: () => void;
|
|
470
|
+
}
|
|
471
|
+
): void;
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
#### Features
|
|
475
|
+
|
|
476
|
+
- Uses `requestAnimationFrame` for optimal timing
|
|
477
|
+
- Separates enter/exit animations
|
|
478
|
+
- Works with CSS transitions
|
|
479
|
+
|
|
480
|
+
#### Example
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
function AnimatedModal({ visible, children }) {
|
|
484
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
485
|
+
|
|
486
|
+
useModalAnimation(visible, {
|
|
487
|
+
onVisible: () => {
|
|
488
|
+
ref.current?.classList.add('fade-in');
|
|
489
|
+
},
|
|
490
|
+
onHidden: () => {
|
|
491
|
+
ref.current?.classList.remove('fade-in');
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return <div ref={ref}>{children}</div>;
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
### useModalDuration
|
|
502
|
+
|
|
503
|
+
Returns modal animation duration.
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
function useModalDuration(modalId?: number): {
|
|
507
|
+
duration: string; // e.g., '300ms'
|
|
508
|
+
milliseconds: number; // e.g., 300
|
|
509
|
+
};
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
### useDestroyAfter
|
|
515
|
+
|
|
516
|
+
Auto-destroys modal after specified time.
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
function useDestroyAfter(
|
|
520
|
+
modalId: number,
|
|
521
|
+
duration: string | number
|
|
522
|
+
): void;
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
#### Behavior
|
|
526
|
+
|
|
527
|
+
- Starts timer when modal becomes hidden
|
|
528
|
+
- Cancels timer if modal becomes visible again
|
|
529
|
+
- Removes modal from DOM after duration
|
|
530
|
+
|
|
531
|
+
#### Example
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
function ToastMessage({ id }) {
|
|
535
|
+
useDestroyAfter(id, 300); // Destroy 300ms after hidden
|
|
536
|
+
return <div>Toast</div>;
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
### useSubscribeModal
|
|
543
|
+
|
|
544
|
+
Subscribes to modal state changes.
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
function useSubscribeModal(modal?: ModalNode): number;
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
#### Returns
|
|
551
|
+
|
|
552
|
+
Version number that increments on each state change.
|
|
553
|
+
|
|
554
|
+
#### Example
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
function ModalDebugger({ modal }) {
|
|
558
|
+
const version = useSubscribeModal(modal);
|
|
559
|
+
|
|
560
|
+
useEffect(() => {
|
|
561
|
+
console.log('State changed:', modal?.visible);
|
|
562
|
+
}, [version]);
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
### useInitializeModal
|
|
569
|
+
|
|
570
|
+
Manually initializes modal service.
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
function useInitializeModal(options?: {
|
|
574
|
+
mode?: 'auto' | 'manual';
|
|
575
|
+
}): {
|
|
576
|
+
initialize: (anchor?: HTMLElement) => void;
|
|
577
|
+
portal: ReactPortal | null;
|
|
578
|
+
};
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
#### Modes
|
|
582
|
+
|
|
583
|
+
| Mode | Description |
|
|
584
|
+
|------|-------------|
|
|
585
|
+
| `auto` | Initializes automatically |
|
|
586
|
+
| `manual` | Requires calling `initialize()` |
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
### useModalOptions
|
|
591
|
+
|
|
592
|
+
Returns modal options configuration.
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
function useModalOptions(): ModalOptions;
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
#### Returns
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
interface ModalOptions {
|
|
602
|
+
duration?: number | string; // Animation duration
|
|
603
|
+
backdrop?: string; // Backdrop overlay color
|
|
604
|
+
manualDestroy?: boolean; // Manual destroy mode
|
|
605
|
+
closeOnBackdropClick?: boolean; // Close on backdrop click
|
|
606
|
+
zIndex?: number; // CSS z-index
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
#### Example
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
function ModalDebugInfo() {
|
|
614
|
+
const options = useModalOptions();
|
|
615
|
+
|
|
616
|
+
return (
|
|
617
|
+
<div>
|
|
618
|
+
<p>Duration: {options.duration}</p>
|
|
619
|
+
<p>Backdrop: {options.backdrop}</p>
|
|
620
|
+
</div>
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
### useModalBackdrop
|
|
628
|
+
|
|
629
|
+
Returns only modal backdrop configuration.
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
function useModalBackdrop(): string | CSSProperties;
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
#### Example
|
|
636
|
+
|
|
637
|
+
```typescript
|
|
638
|
+
function BackdropInfo() {
|
|
639
|
+
const backdrop = useModalBackdrop();
|
|
640
|
+
|
|
641
|
+
return <p>Current backdrop: {backdrop}</p>;
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Components
|
|
648
|
+
|
|
649
|
+
### ModalProvider
|
|
650
|
+
|
|
651
|
+
Main provider component for initialization.
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
interface ModalProviderProps {
|
|
655
|
+
children: ReactNode;
|
|
656
|
+
ForegroundComponent?: ComponentType<ModalFrameProps>;
|
|
657
|
+
BackgroundComponent?: ComponentType<ModalFrameProps>;
|
|
658
|
+
TitleComponent?: ComponentType<WrapperComponentProps>;
|
|
659
|
+
SubtitleComponent?: ComponentType<WrapperComponentProps>;
|
|
660
|
+
ContentComponent?: ComponentType<WrapperComponentProps>;
|
|
661
|
+
FooterComponent?: ComponentType<FooterComponentProps>;
|
|
662
|
+
options?: ModalOptions;
|
|
663
|
+
context?: Record<string, any>;
|
|
664
|
+
usePathname?: () => { pathname: string }; // Router integration
|
|
665
|
+
root?: HTMLElement; // Custom root element
|
|
666
|
+
}
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
#### ModalOptions
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
interface ModalOptions {
|
|
673
|
+
duration?: number | string;
|
|
674
|
+
backdrop?: string;
|
|
675
|
+
manualDestroy?: boolean;
|
|
676
|
+
closeOnBackdropClick?: boolean;
|
|
677
|
+
}
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
#### usePathname (Router Integration)
|
|
681
|
+
|
|
682
|
+
The `usePathname` prop enables router integration. Modals will automatically close when the route changes.
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
import { useLocation } from 'react-router-dom';
|
|
686
|
+
|
|
687
|
+
<ModalProvider
|
|
688
|
+
usePathname={useLocation} // react-router-dom integration
|
|
689
|
+
// ...
|
|
690
|
+
>
|
|
691
|
+
<App />
|
|
692
|
+
</ModalProvider>
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
#### Example
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
import { useLocation } from 'react-router-dom';
|
|
699
|
+
|
|
700
|
+
<ModalProvider
|
|
701
|
+
usePathname={useLocation}
|
|
702
|
+
ForegroundComponent={CustomForeground}
|
|
703
|
+
TitleComponent={CustomTitle}
|
|
704
|
+
SubtitleComponent={CustomSubtitle}
|
|
705
|
+
ContentComponent={CustomContent}
|
|
706
|
+
FooterComponent={CustomFooter}
|
|
707
|
+
options={{
|
|
708
|
+
duration: '200ms',
|
|
709
|
+
backdrop: 'rgba(0, 0, 0, 0.35)',
|
|
710
|
+
manualDestroy: true,
|
|
711
|
+
closeOnBackdropClick: true,
|
|
712
|
+
}}
|
|
713
|
+
context={{
|
|
714
|
+
theme: 'dark',
|
|
715
|
+
locale: 'en-US',
|
|
716
|
+
}}
|
|
717
|
+
>
|
|
718
|
+
<App />
|
|
719
|
+
</ModalProvider>
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
### Custom Components
|
|
725
|
+
|
|
726
|
+
#### ModalFrameProps
|
|
727
|
+
|
|
728
|
+
Props passed to Foreground/Background components.
|
|
729
|
+
|
|
730
|
+
```typescript
|
|
731
|
+
interface ModalFrameProps<Context = any, B = any> {
|
|
732
|
+
id: number;
|
|
733
|
+
type: 'alert' | 'confirm' | 'prompt';
|
|
734
|
+
alive: boolean;
|
|
735
|
+
visible: boolean;
|
|
736
|
+
initiator: string;
|
|
737
|
+
manualDestroy: boolean;
|
|
738
|
+
closeOnBackdropClick: boolean;
|
|
739
|
+
background?: ModalBackground<B>;
|
|
740
|
+
onConfirm: () => void;
|
|
741
|
+
onClose: () => void;
|
|
742
|
+
onChange: (value: any) => void;
|
|
743
|
+
onDestroy: () => void;
|
|
744
|
+
onChangeOrder: Function;
|
|
745
|
+
context: Context;
|
|
746
|
+
children: ReactNode;
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
#### FooterComponentProps
|
|
751
|
+
|
|
752
|
+
Props for footer components.
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
interface FooterComponentProps {
|
|
756
|
+
type: 'alert' | 'confirm' | 'prompt';
|
|
757
|
+
onConfirm: (value?: any) => void;
|
|
758
|
+
onClose: () => void;
|
|
759
|
+
onCancel?: () => void;
|
|
760
|
+
disabled?: boolean;
|
|
761
|
+
footer?: FooterOptions;
|
|
762
|
+
context: any;
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
#### WrapperComponentProps
|
|
767
|
+
|
|
768
|
+
Props for title/subtitle/content components.
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
interface WrapperComponentProps {
|
|
772
|
+
children: ReactNode;
|
|
773
|
+
context: any;
|
|
774
|
+
}
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
#### Custom Component Example
|
|
778
|
+
|
|
779
|
+
```typescript
|
|
780
|
+
const CustomForeground: FC<ModalFrameProps> = ({
|
|
781
|
+
id,
|
|
782
|
+
visible,
|
|
783
|
+
children,
|
|
784
|
+
onClose,
|
|
785
|
+
}) => {
|
|
786
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
787
|
+
const { duration } = useModalDuration();
|
|
788
|
+
|
|
789
|
+
useModalAnimation(visible, {
|
|
790
|
+
onVisible: () => ref.current?.classList.add('visible'),
|
|
791
|
+
onHidden: () => ref.current?.classList.remove('visible'),
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
useDestroyAfter(id, duration);
|
|
795
|
+
|
|
796
|
+
return (
|
|
797
|
+
<div
|
|
798
|
+
ref={ref}
|
|
799
|
+
style={{
|
|
800
|
+
background: 'white',
|
|
801
|
+
borderRadius: 12,
|
|
802
|
+
padding: 24,
|
|
803
|
+
opacity: 0,
|
|
804
|
+
transition: `opacity ${duration}ms`,
|
|
805
|
+
}}
|
|
806
|
+
>
|
|
807
|
+
{children}
|
|
808
|
+
</div>
|
|
809
|
+
);
|
|
810
|
+
};
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
## Type Definitions
|
|
816
|
+
|
|
817
|
+
### Modal Types
|
|
818
|
+
|
|
819
|
+
```typescript
|
|
820
|
+
type ModalType = 'alert' | 'confirm' | 'prompt';
|
|
821
|
+
type ModalSubtype = 'info' | 'success' | 'warning' | 'error';
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### ModalBackground
|
|
825
|
+
|
|
826
|
+
```typescript
|
|
827
|
+
interface ModalBackground<T = any> {
|
|
828
|
+
data?: T;
|
|
829
|
+
[key: string]: any;
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Content Props
|
|
834
|
+
|
|
835
|
+
```typescript
|
|
836
|
+
interface AlertContentProps {
|
|
837
|
+
onConfirm: () => void;
|
|
838
|
+
context: any;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
interface ConfirmContentProps {
|
|
842
|
+
onConfirm: () => void;
|
|
843
|
+
onCancel: () => void;
|
|
844
|
+
context: any;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
interface PromptContentProps<T> {
|
|
848
|
+
value?: T;
|
|
849
|
+
onChange: (value: T) => void;
|
|
850
|
+
onConfirm: () => void;
|
|
851
|
+
onCancel: () => void;
|
|
852
|
+
context: any;
|
|
853
|
+
}
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
## Usage Patterns
|
|
859
|
+
|
|
860
|
+
### Pattern 1: Static API (Simplest)
|
|
861
|
+
|
|
862
|
+
```typescript
|
|
863
|
+
import { alert, confirm, prompt } from '@lerx/promise-modal';
|
|
864
|
+
|
|
865
|
+
// Use anywhere, even outside React
|
|
866
|
+
async function handleSubmit() {
|
|
867
|
+
if (await confirm({ content: 'Save changes?' })) {
|
|
868
|
+
await saveData();
|
|
869
|
+
await alert({ content: 'Saved!' });
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
### Pattern 2: useModal Hook (Recommended for Components)
|
|
875
|
+
|
|
876
|
+
```typescript
|
|
877
|
+
function EditForm() {
|
|
878
|
+
const modal = useModal();
|
|
879
|
+
|
|
880
|
+
const handleSave = async () => {
|
|
881
|
+
if (await modal.confirm({ content: 'Save?' })) {
|
|
882
|
+
// Modals auto-cleanup if component unmounts
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
### Pattern 3: Full Customization
|
|
889
|
+
|
|
890
|
+
```typescript
|
|
891
|
+
<ModalProvider
|
|
892
|
+
ForegroundComponent={CustomForeground}
|
|
893
|
+
FooterComponent={CustomFooter}
|
|
894
|
+
options={{ duration: 400 }}
|
|
895
|
+
context={{ theme: 'dark' }}
|
|
896
|
+
>
|
|
897
|
+
<App />
|
|
898
|
+
</ModalProvider>
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
### Pattern 4: Per-Modal Override
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
await alert({
|
|
905
|
+
content: 'Special modal',
|
|
906
|
+
ForegroundComponent: SpecialForeground,
|
|
907
|
+
background: { variant: 'special' },
|
|
908
|
+
});
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
## Advanced Examples
|
|
914
|
+
|
|
915
|
+
### Toast Notifications
|
|
916
|
+
|
|
917
|
+
```typescript
|
|
918
|
+
const ToastForeground: FC<ModalFrameProps> = ({
|
|
919
|
+
id,
|
|
920
|
+
visible,
|
|
921
|
+
children,
|
|
922
|
+
onClose,
|
|
923
|
+
}) => {
|
|
924
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
925
|
+
const { duration } = useModalDuration();
|
|
926
|
+
|
|
927
|
+
useEffect(() => {
|
|
928
|
+
const timer = setTimeout(onClose, 3000);
|
|
929
|
+
return () => clearTimeout(timer);
|
|
930
|
+
}, [onClose]);
|
|
931
|
+
|
|
932
|
+
useModalAnimation(visible, {
|
|
933
|
+
onVisible: () => ref.current?.classList.add('visible'),
|
|
934
|
+
onHidden: () => ref.current?.classList.remove('visible'),
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
useDestroyAfter(id, duration);
|
|
938
|
+
|
|
939
|
+
return (
|
|
940
|
+
<div
|
|
941
|
+
ref={ref}
|
|
942
|
+
style={{
|
|
943
|
+
position: 'fixed',
|
|
944
|
+
bottom: 20,
|
|
945
|
+
left: '50%',
|
|
946
|
+
transform: 'translateX(-50%) translateY(100px)',
|
|
947
|
+
opacity: 0,
|
|
948
|
+
transition: `all ${duration}ms`,
|
|
949
|
+
}}
|
|
950
|
+
>
|
|
951
|
+
{children}
|
|
952
|
+
</div>
|
|
953
|
+
);
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
export const toast = (message: ReactNode) => {
|
|
957
|
+
return alert({
|
|
958
|
+
content: message,
|
|
959
|
+
ForegroundComponent: ToastForeground,
|
|
960
|
+
footer: false,
|
|
961
|
+
dimmed: false,
|
|
962
|
+
closeOnBackdropClick: false,
|
|
963
|
+
});
|
|
964
|
+
};
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### Multi-Step Wizard
|
|
968
|
+
|
|
969
|
+
```typescript
|
|
970
|
+
async function registrationWizard() {
|
|
971
|
+
// Step 1: Terms acceptance
|
|
972
|
+
const accepted = await confirm({
|
|
973
|
+
title: 'Terms of Service',
|
|
974
|
+
content: <TermsContent />,
|
|
975
|
+
footer: { confirm: 'I Accept', cancel: 'Decline' },
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
if (!accepted) return null;
|
|
979
|
+
|
|
980
|
+
// Step 2: User information
|
|
981
|
+
const userInfo = await prompt<{
|
|
982
|
+
name: string;
|
|
983
|
+
email: string;
|
|
984
|
+
}>({
|
|
985
|
+
title: 'Your Information',
|
|
986
|
+
defaultValue: { name: '', email: '' },
|
|
987
|
+
Input: RegistrationForm,
|
|
988
|
+
disabled: (v) => !v.name || !v.email?.includes('@'),
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
if (!userInfo) return null;
|
|
992
|
+
|
|
993
|
+
// Step 3: Confirmation
|
|
994
|
+
await alert({
|
|
995
|
+
title: 'Welcome!',
|
|
996
|
+
content: `Account created for ${userInfo.name}`,
|
|
997
|
+
subtype: 'success',
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
return userInfo;
|
|
1001
|
+
}
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### Custom Anchor (Iframe/Portal)
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
function IframedModals() {
|
|
1008
|
+
const { initialize } = useInitializeModal({ mode: 'manual' });
|
|
1009
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
1010
|
+
|
|
1011
|
+
useEffect(() => {
|
|
1012
|
+
if (containerRef.current) {
|
|
1013
|
+
initialize(containerRef.current);
|
|
1014
|
+
}
|
|
1015
|
+
}, [initialize]);
|
|
1016
|
+
|
|
1017
|
+
return (
|
|
1018
|
+
<div style={{ position: 'relative', height: 600 }}>
|
|
1019
|
+
<div ref={containerRef} style={{ height: '100%' }} />
|
|
1020
|
+
<ModalTriggerButtons />
|
|
1021
|
+
</div>
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
---
|
|
1027
|
+
|
|
1028
|
+
## Lifecycle
|
|
1029
|
+
|
|
1030
|
+
```
|
|
1031
|
+
Creation → Show → Hide → Destroy
|
|
1032
|
+
↓ ↓ ↓ ↓
|
|
1033
|
+
open() visible onHide onDestroy
|
|
1034
|
+
↓ true ↓ ↓
|
|
1035
|
+
nodeFactory ↓ visible alive
|
|
1036
|
+
↓ ↓ false false
|
|
1037
|
+
Promise animation ↓ ↓
|
|
1038
|
+
↓ starts duration removed
|
|
1039
|
+
... ↓ passes from DOM
|
|
1040
|
+
↓ ↓
|
|
1041
|
+
interaction destroy
|
|
1042
|
+
↓
|
|
1043
|
+
resolve
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
---
|
|
1047
|
+
|
|
1048
|
+
## Best Practices
|
|
1049
|
+
|
|
1050
|
+
1. **Wrap app with ModalProvider** at the root level
|
|
1051
|
+
2. **Use useModal in components** for automatic cleanup
|
|
1052
|
+
3. **Use static API for utilities** outside React
|
|
1053
|
+
4. **Customize at provider level** for consistent styling
|
|
1054
|
+
5. **Use subtype for semantics** (info, success, warning, error)
|
|
1055
|
+
6. **Always await or handle** promise rejections
|
|
1056
|
+
7. **Keep modal content simple** - avoid complex state
|
|
1057
|
+
8. **Test accessibility** - ensure keyboard navigation
|
|
1058
|
+
|
|
1059
|
+
---
|
|
1060
|
+
|
|
1061
|
+
## AbortSignal Support
|
|
1062
|
+
|
|
1063
|
+
Provides `AbortSignal` support for programmatically canceling modals.
|
|
1064
|
+
|
|
1065
|
+
### Basic Usage
|
|
1066
|
+
|
|
1067
|
+
```typescript
|
|
1068
|
+
const controller = new AbortController();
|
|
1069
|
+
|
|
1070
|
+
alert({
|
|
1071
|
+
title: 'Cancelable Modal',
|
|
1072
|
+
content: 'This will auto-close in 3 seconds.',
|
|
1073
|
+
signal: controller.signal,
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// Cancel modal after 3 seconds
|
|
1077
|
+
setTimeout(() => {
|
|
1078
|
+
controller.abort();
|
|
1079
|
+
}, 3000);
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
### Manual Abort Control
|
|
1083
|
+
|
|
1084
|
+
```typescript
|
|
1085
|
+
function ManualAbortControl() {
|
|
1086
|
+
const [controller, setController] = useState<AbortController | null>(null);
|
|
1087
|
+
|
|
1088
|
+
const handleOpen = () => {
|
|
1089
|
+
const newController = new AbortController();
|
|
1090
|
+
setController(newController);
|
|
1091
|
+
|
|
1092
|
+
alert({
|
|
1093
|
+
title: 'Manual Cancel',
|
|
1094
|
+
content: 'Click "Cancel" button to close this modal.',
|
|
1095
|
+
signal: newController.signal,
|
|
1096
|
+
closeOnBackdropClick: false,
|
|
1097
|
+
}).then(() => {
|
|
1098
|
+
setController(null);
|
|
1099
|
+
});
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
const handleAbort = () => {
|
|
1103
|
+
if (controller) {
|
|
1104
|
+
controller.abort();
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
return (
|
|
1109
|
+
<div>
|
|
1110
|
+
<button onClick={handleOpen} disabled={!!controller}>
|
|
1111
|
+
Open Modal
|
|
1112
|
+
</button>
|
|
1113
|
+
<button onClick={handleAbort} disabled={!controller}>
|
|
1114
|
+
Cancel Modal
|
|
1115
|
+
</button>
|
|
1116
|
+
</div>
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
### Batch Cancel Multiple Modals
|
|
1122
|
+
|
|
1123
|
+
```typescript
|
|
1124
|
+
function MultipleModalsAbort() {
|
|
1125
|
+
const [controllers, setControllers] = useState<AbortController[]>([]);
|
|
1126
|
+
|
|
1127
|
+
const handleOpenMultiple = () => {
|
|
1128
|
+
const newControllers: AbortController[] = [];
|
|
1129
|
+
|
|
1130
|
+
for (let i = 0; i < 3; i++) {
|
|
1131
|
+
const controller = new AbortController();
|
|
1132
|
+
newControllers.push(controller);
|
|
1133
|
+
|
|
1134
|
+
alert({
|
|
1135
|
+
title: `Modal ${i + 1}`,
|
|
1136
|
+
content: `This is modal number ${i + 1}.`,
|
|
1137
|
+
signal: controller.signal,
|
|
1138
|
+
closeOnBackdropClick: false,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
setControllers(newControllers);
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
const handleAbortAll = () => {
|
|
1146
|
+
controllers.forEach((controller) => controller.abort());
|
|
1147
|
+
setControllers([]);
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
return (
|
|
1151
|
+
<div>
|
|
1152
|
+
<button onClick={handleOpenMultiple}>Open 3 Modals</button>
|
|
1153
|
+
<button onClick={handleAbortAll}>Cancel All Modals</button>
|
|
1154
|
+
</div>
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
### Pre-Aborted Signal Handling
|
|
1160
|
+
|
|
1161
|
+
```typescript
|
|
1162
|
+
// If the signal is already aborted, the modal closes immediately
|
|
1163
|
+
const controller = new AbortController();
|
|
1164
|
+
controller.abort(); // Abort first
|
|
1165
|
+
|
|
1166
|
+
alert({
|
|
1167
|
+
title: 'Instant Close',
|
|
1168
|
+
content: 'Signal is already aborted, closing immediately.',
|
|
1169
|
+
signal: controller.signal,
|
|
1170
|
+
}).then(() => {
|
|
1171
|
+
console.log('Modal closed immediately.');
|
|
1172
|
+
});
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
---
|
|
1176
|
+
|
|
1177
|
+
## License
|
|
1178
|
+
|
|
1179
|
+
MIT License
|
|
1180
|
+
|
|
1181
|
+
---
|
|
1182
|
+
|
|
1183
|
+
## Version
|
|
1184
|
+
|
|
1185
|
+
Current: See package.json
|