@okyrychenko-dev/react-action-guard 0.6.0 → 0.6.1
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/dist/index.cjs +161 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1200 -60
- package/dist/index.d.ts +1200 -60
- package/dist/index.js +161 -27
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -13,19 +13,131 @@ interface UseAsyncActionOptions {
|
|
|
13
13
|
onTimeout?: (blockerId: string) => void;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Wraps async functions with automatic UI blocking during execution.
|
|
17
17
|
*
|
|
18
|
-
* Creates a wrapper function that automatically
|
|
19
|
-
*
|
|
18
|
+
* Creates a wrapper function that automatically:
|
|
19
|
+
* 1. Adds a blocker before executing the async function
|
|
20
|
+
* 2. Executes the async function
|
|
21
|
+
* 3. Removes the blocker after completion (success or failure)
|
|
20
22
|
*
|
|
21
|
-
*
|
|
23
|
+
* Each invocation generates a unique blocker ID (actionId-counter), allowing
|
|
24
|
+
* multiple concurrent executions of the same action without ID conflicts.
|
|
25
|
+
* The blocker is removed even if the async function throws an error.
|
|
22
26
|
*
|
|
23
|
-
*
|
|
24
|
-
* @param actionId - Unique identifier for the action (used in blocker ID and reason)
|
|
25
|
-
* @param scope - Scope(s) to block during execution (default: "global")
|
|
26
|
-
* @param options - Optional configuration including timeout
|
|
27
|
-
* @returns Function that wraps async functions with blocking behavior
|
|
27
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
28
28
|
*
|
|
29
|
+
* @template T - The return type of the async function being wrapped
|
|
30
|
+
* @param actionId - Base identifier for the action. Used in blocker ID and default reason.
|
|
31
|
+
* Example: "saveForm", "fetchData", "submitOrder"
|
|
32
|
+
* @param scope - Scope(s) to block during execution. Can be a single scope or array.
|
|
33
|
+
* Defaults to "global" if not specified.
|
|
34
|
+
* @param options - Optional configuration
|
|
35
|
+
* @param options.timeout - Timeout in milliseconds. Blocker auto-removes after this duration.
|
|
36
|
+
* @param options.onTimeout - Callback invoked when blocker times out before async completes
|
|
37
|
+
*
|
|
38
|
+
* @returns Wrapper function that accepts an async function and returns a Promise with the same type
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* Basic form submission
|
|
42
|
+
* ```tsx
|
|
43
|
+
* function MyForm() {
|
|
44
|
+
* const executeWithBlocking = useAsyncAction('saveForm', 'form');
|
|
45
|
+
*
|
|
46
|
+
* const handleSubmit = async (e) => {
|
|
47
|
+
* e.preventDefault();
|
|
48
|
+
*
|
|
49
|
+
* await executeWithBlocking(async () => {
|
|
50
|
+
* const response = await fetch('/api/save', {
|
|
51
|
+
* method: 'POST',
|
|
52
|
+
* body: JSON.stringify(formData)
|
|
53
|
+
* });
|
|
54
|
+
* return response.json();
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* alert('Form saved!');
|
|
58
|
+
* };
|
|
59
|
+
*
|
|
60
|
+
* return <form onSubmit={handleSubmit}>...</form>;
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* With timeout and error handling
|
|
66
|
+
* ```tsx
|
|
67
|
+
* const executeWithBlocking = useAsyncAction(
|
|
68
|
+
* 'fetchData',
|
|
69
|
+
* ['global', 'data'],
|
|
70
|
+
* {
|
|
71
|
+
* timeout: 30000, // 30 seconds
|
|
72
|
+
* onTimeout: (blockerId) => {
|
|
73
|
+
* console.warn(`Operation ${blockerId} timed out`);
|
|
74
|
+
* }
|
|
75
|
+
* }
|
|
76
|
+
* );
|
|
77
|
+
*
|
|
78
|
+
* const loadData = async () => {
|
|
79
|
+
* try {
|
|
80
|
+
* const data = await executeWithBlocking(async () => {
|
|
81
|
+
* return await fetchDataFromAPI();
|
|
82
|
+
* });
|
|
83
|
+
* setData(data);
|
|
84
|
+
* } catch (error) {
|
|
85
|
+
* console.error('Failed to load data:', error);
|
|
86
|
+
* // Blocker is automatically removed even on error
|
|
87
|
+
* }
|
|
88
|
+
* };
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* Multiple concurrent actions
|
|
93
|
+
* ```tsx
|
|
94
|
+
* function FileUploader() {
|
|
95
|
+
* const uploadWithBlocking = useAsyncAction('upload', 'uploads');
|
|
96
|
+
*
|
|
97
|
+
* const uploadFiles = async (files) => {
|
|
98
|
+
* // Each upload gets unique blocker ID (upload-1, upload-2, etc.)
|
|
99
|
+
* const promises = files.map(file =>
|
|
100
|
+
* uploadWithBlocking(() => uploadFile(file))
|
|
101
|
+
* );
|
|
102
|
+
*
|
|
103
|
+
* await Promise.all(promises);
|
|
104
|
+
* };
|
|
105
|
+
*
|
|
106
|
+
* return <...>;
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* Type-safe return value
|
|
112
|
+
* ```tsx
|
|
113
|
+
* interface UserData {
|
|
114
|
+
* id: string;
|
|
115
|
+
* name: string;
|
|
116
|
+
* }
|
|
117
|
+
*
|
|
118
|
+
* function UserProfile() {
|
|
119
|
+
* const executeWithBlocking = useAsyncAction<UserData>('fetchUser', 'user');
|
|
120
|
+
*
|
|
121
|
+
* const loadUser = async (userId: string) => {
|
|
122
|
+
* const userData = await executeWithBlocking(async () => {
|
|
123
|
+
* const response = await fetch(`/api/users/${userId}`);
|
|
124
|
+
* return response.json(); // TypeScript knows this is UserData
|
|
125
|
+
* });
|
|
126
|
+
*
|
|
127
|
+
* // userData is typed as UserData
|
|
128
|
+
* console.log(userData.name);
|
|
129
|
+
* };
|
|
130
|
+
*
|
|
131
|
+
* return <...>;
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* @see {@link useBlocker} for manual blocker management
|
|
136
|
+
* @see {@link useIsBlocked} to check blocking state
|
|
137
|
+
* @see {@link UseAsyncActionOptions} for available options
|
|
138
|
+
*
|
|
139
|
+
* @public
|
|
140
|
+
* @since 0.6.0
|
|
29
141
|
*/
|
|
30
142
|
declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>, options?: UseAsyncActionOptions) => ((asyncFn: () => Promise<T>) => Promise<T>);
|
|
31
143
|
|
|
@@ -58,19 +170,195 @@ interface MiddlewareContext {
|
|
|
58
170
|
}
|
|
59
171
|
type Middleware = (context: MiddlewareContext) => void | Promise<void>;
|
|
60
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Creates middleware for tracking blocker events to analytics platforms.
|
|
175
|
+
*
|
|
176
|
+
* Automatically tracks all blocker actions (add, remove, update, timeout, clear)
|
|
177
|
+
* to your analytics platform. Supports Google Analytics, Mixpanel, Amplitude,
|
|
178
|
+
* and custom analytics providers.
|
|
179
|
+
*
|
|
180
|
+
* Event data includes: blockerId, action, scope, reason, priority, timestamp,
|
|
181
|
+
* and duration (for remove events).
|
|
182
|
+
*
|
|
183
|
+
* @param config - Analytics configuration specifying provider and credentials
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* Google Analytics integration
|
|
187
|
+
* ```ts
|
|
188
|
+
* import { configureMiddleware, createAnalyticsMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
189
|
+
*
|
|
190
|
+
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
191
|
+
* provider: 'ga',
|
|
192
|
+
* trackingId: 'GA-XXXXX-Y'
|
|
193
|
+
* });
|
|
194
|
+
*
|
|
195
|
+
* configureMiddleware([analyticsMiddleware]);
|
|
196
|
+
* ```
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* Mixpanel integration
|
|
200
|
+
* ```ts
|
|
201
|
+
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
202
|
+
* provider: 'mixpanel',
|
|
203
|
+
* token: 'your_mixpanel_token',
|
|
204
|
+
* options: {
|
|
205
|
+
* debug: true
|
|
206
|
+
* }
|
|
207
|
+
* });
|
|
208
|
+
*
|
|
209
|
+
* configureMiddleware([analyticsMiddleware]);
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* Custom analytics provider
|
|
214
|
+
* ```ts
|
|
215
|
+
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
216
|
+
* track: (eventName, eventData) => {
|
|
217
|
+
* // Send to your custom analytics
|
|
218
|
+
* myAnalytics.track(eventName, {
|
|
219
|
+
* ...eventData,
|
|
220
|
+
* userId: getCurrentUserId()
|
|
221
|
+
* });
|
|
222
|
+
* }
|
|
223
|
+
* });
|
|
224
|
+
* ```
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* Amplitude with user properties
|
|
228
|
+
* ```ts
|
|
229
|
+
* const analyticsMiddleware = createAnalyticsMiddleware({
|
|
230
|
+
* provider: 'amplitude',
|
|
231
|
+
* apiKey: 'your_amplitude_key',
|
|
232
|
+
* userId: 'user-123',
|
|
233
|
+
* userProperties: {
|
|
234
|
+
* plan: 'premium',
|
|
235
|
+
* role: 'admin'
|
|
236
|
+
* }
|
|
237
|
+
* });
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* @see {@link AnalyticsConfig} for configuration options
|
|
241
|
+
* @see {@link configureMiddleware} for registering middleware
|
|
242
|
+
*
|
|
243
|
+
* @public
|
|
244
|
+
* @since 0.6.0
|
|
245
|
+
*/
|
|
61
246
|
declare const createAnalyticsMiddleware: (config: AnalyticsConfig) => Middleware;
|
|
62
247
|
|
|
63
248
|
/**
|
|
64
|
-
*
|
|
249
|
+
* Configures and registers multiple middleware functions for the UI blocking store.
|
|
250
|
+
*
|
|
251
|
+
* Convenience function that registers an array of middleware at once. Each middleware
|
|
252
|
+
* is automatically assigned a unique name based on its array index ("middleware-0",
|
|
253
|
+
* "middleware-1", etc.). Middleware are executed in the order they are registered.
|
|
254
|
+
*
|
|
255
|
+
* All middleware receive {@link MiddlewareContext} containing action type, blocker ID,
|
|
256
|
+
* configuration, and timestamp for each blocking event.
|
|
65
257
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
258
|
+
* @param middlewares - Array of middleware functions to register. Functions are called
|
|
259
|
+
* in order for each blocking action (add, remove, update, etc.)
|
|
68
260
|
*
|
|
69
|
-
* @
|
|
261
|
+
* @example
|
|
262
|
+
* Basic usage with built-in middleware
|
|
263
|
+
* ```ts
|
|
264
|
+
* import { configureMiddleware, loggerMiddleware, createPerformanceMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
265
|
+
*
|
|
266
|
+
* configureMiddleware([
|
|
267
|
+
* loggerMiddleware,
|
|
268
|
+
* createPerformanceMiddleware({ threshold: 1000 })
|
|
269
|
+
* ]);
|
|
270
|
+
* ```
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* With custom middleware
|
|
274
|
+
* ```ts
|
|
275
|
+
* const customMiddleware: Middleware = (context) => {
|
|
276
|
+
* if (context.action === 'add') {
|
|
277
|
+
* console.log(`Blocker added: ${context.blockerId}`);
|
|
278
|
+
* }
|
|
279
|
+
* };
|
|
280
|
+
*
|
|
281
|
+
* configureMiddleware([
|
|
282
|
+
* loggerMiddleware,
|
|
283
|
+
* customMiddleware
|
|
284
|
+
* ]);
|
|
285
|
+
* ```
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* Analytics integration
|
|
289
|
+
* ```ts
|
|
290
|
+
* import { configureMiddleware, createAnalyticsMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
70
291
|
*
|
|
292
|
+
* configureMiddleware([
|
|
293
|
+
* createAnalyticsMiddleware({
|
|
294
|
+
* provider: 'google-analytics',
|
|
295
|
+
* trackingId: 'GA-XXXXX'
|
|
296
|
+
* })
|
|
297
|
+
* ]);
|
|
298
|
+
* ```
|
|
299
|
+
*
|
|
300
|
+
* @see {@link Middleware} for middleware function signature
|
|
301
|
+
* @see {@link loggerMiddleware} for built-in logging middleware
|
|
302
|
+
* @see {@link createAnalyticsMiddleware} for analytics tracking
|
|
303
|
+
* @see {@link createPerformanceMiddleware} for performance monitoring
|
|
304
|
+
*
|
|
305
|
+
* @public
|
|
306
|
+
* @since 0.6.0
|
|
71
307
|
*/
|
|
72
308
|
declare function configureMiddleware(middlewares: ReadonlyArray<Middleware>): void;
|
|
73
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Built-in middleware for logging all blocking actions to the console.
|
|
312
|
+
*
|
|
313
|
+
* Logs every blocker action (add, remove, update, timeout, clear) with emoji
|
|
314
|
+
* indicators and formatted data. Useful for debugging blocking behavior during
|
|
315
|
+
* development. Automatically disabled in production builds for performance.
|
|
316
|
+
*
|
|
317
|
+
* Output format: `[UIBlocking] 🔒 blocker-id {scope: "form", reason: "..."}`
|
|
318
|
+
*
|
|
319
|
+
* Emoji indicators:
|
|
320
|
+
* - 🔒 Add
|
|
321
|
+
* - 🔓 Remove
|
|
322
|
+
* - 🔄 Update
|
|
323
|
+
* - ⏰ Timeout
|
|
324
|
+
* - 🧹 Clear/ClearScope
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* Register logger middleware
|
|
328
|
+
* ```ts
|
|
329
|
+
* import { configureMiddleware, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
330
|
+
*
|
|
331
|
+
* // Enable logging for all blocking actions
|
|
332
|
+
* configureMiddleware([loggerMiddleware]);
|
|
333
|
+
* ```
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* Console output examples
|
|
337
|
+
* ```
|
|
338
|
+
* [UIBlocking] 🔒 save-form {scope: "form", reason: "Saving...", priority: 50}
|
|
339
|
+
* [UIBlocking] 🔄 save-form {scope: "form", reason: "Almost done..."}
|
|
340
|
+
* [UIBlocking] 🔓 save-form {reason: "Saving..."}
|
|
341
|
+
* [UIBlocking] ⏰ api-timeout {timeout: 30000}
|
|
342
|
+
* ```
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* Production check pattern
|
|
346
|
+
* ```ts
|
|
347
|
+
* import { configureMiddleware, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
348
|
+
* const middlewares = [];
|
|
349
|
+
* if (process.env.NODE_ENV !== 'production') {
|
|
350
|
+
* middlewares.push(loggerMiddleware);
|
|
351
|
+
* }
|
|
352
|
+
* configureMiddleware(middlewares);
|
|
353
|
+
* ```
|
|
354
|
+
*
|
|
355
|
+
* @see {@link configureMiddleware} for registering middleware
|
|
356
|
+
* @see {@link Middleware} for middleware function signature
|
|
357
|
+
* @see {@link MiddlewareContext} for available context data
|
|
358
|
+
*
|
|
359
|
+
* @public
|
|
360
|
+
* @since 0.6.0
|
|
361
|
+
*/
|
|
74
362
|
declare const loggerMiddleware: Middleware;
|
|
75
363
|
|
|
76
364
|
interface PerformanceConfig {
|
|
@@ -78,10 +366,128 @@ interface PerformanceConfig {
|
|
|
78
366
|
onSlowBlock?: (blockerId: string, duration: number) => void;
|
|
79
367
|
}
|
|
80
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Creates middleware for monitoring blocker performance and detecting slow blocks.
|
|
371
|
+
*
|
|
372
|
+
* Tracks the duration of each blocker (from add to remove) and warns when a blocker
|
|
373
|
+
* exceeds the configured threshold. Useful for identifying performance issues,
|
|
374
|
+
* stuck blockers, or operations that take too long.
|
|
375
|
+
*
|
|
376
|
+
* Automatically logs warnings to console for slow blocks and optionally invokes
|
|
377
|
+
* a callback for custom handling (e.g., error reporting, analytics).
|
|
378
|
+
*
|
|
379
|
+
* @param config - Performance monitoring configuration
|
|
380
|
+
* @param config.slowBlockThreshold - Duration in ms before considering a block "slow".
|
|
381
|
+
* Defaults to 3000ms (3 seconds).
|
|
382
|
+
* @param config.onSlowBlock - Optional callback invoked when a slow block is detected.
|
|
383
|
+
* Receives blockerId and duration in milliseconds.
|
|
384
|
+
*
|
|
385
|
+
* @returns Middleware function for performance monitoring
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* Basic usage with default threshold
|
|
389
|
+
* ```ts
|
|
390
|
+
* import { configureMiddleware, createPerformanceMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
391
|
+
*
|
|
392
|
+
* const performanceMiddleware = createPerformanceMiddleware();
|
|
393
|
+
*
|
|
394
|
+
* configureMiddleware([performanceMiddleware]);
|
|
395
|
+
* // Warns if any blocker lasts > 3 seconds
|
|
396
|
+
* ```
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* Custom threshold and callback
|
|
400
|
+
* ```ts
|
|
401
|
+
* const performanceMiddleware = createPerformanceMiddleware({
|
|
402
|
+
* slowBlockThreshold: 5000, // 5 seconds
|
|
403
|
+
* onSlowBlock: (blockerId, duration) => {
|
|
404
|
+
* console.error(`Blocker ${blockerId} took ${duration}ms`);
|
|
405
|
+
*
|
|
406
|
+
* // Report to error tracking
|
|
407
|
+
* Sentry.captureMessage('Slow UI Blocker', {
|
|
408
|
+
* extra: { blockerId, duration }
|
|
409
|
+
* });
|
|
410
|
+
* }
|
|
411
|
+
* });
|
|
412
|
+
* ```
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* With analytics integration
|
|
416
|
+
* ```ts
|
|
417
|
+
* const performanceMiddleware = createPerformanceMiddleware({
|
|
418
|
+
* slowBlockThreshold: 2000,
|
|
419
|
+
* onSlowBlock: (blockerId, duration) => {
|
|
420
|
+
* // Track slow blocks in analytics
|
|
421
|
+
* analytics.track('slow_blocker_detected', {
|
|
422
|
+
* blocker_id: blockerId,
|
|
423
|
+
* duration_ms: duration,
|
|
424
|
+
* threshold_ms: 2000
|
|
425
|
+
* });
|
|
426
|
+
* }
|
|
427
|
+
* });
|
|
428
|
+
* ```
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* Development vs Production
|
|
432
|
+
* ```ts
|
|
433
|
+
* const performanceMiddleware = createPerformanceMiddleware({
|
|
434
|
+
* // Strict threshold in development
|
|
435
|
+
* slowBlockThreshold: process.env.NODE_ENV === 'production' ? 5000 : 1000,
|
|
436
|
+
* onSlowBlock: (blockerId, duration) => {
|
|
437
|
+
* if (process.env.NODE_ENV === 'production') {
|
|
438
|
+
* // Report to error service in production
|
|
439
|
+
* errorService.log({ blockerId, duration });
|
|
440
|
+
* } else {
|
|
441
|
+
* // Detailed logging in development
|
|
442
|
+
* console.table({ blockerId, duration });
|
|
443
|
+
* }
|
|
444
|
+
* }
|
|
445
|
+
* });
|
|
446
|
+
* ```
|
|
447
|
+
*
|
|
448
|
+
* @see {@link PerformanceConfig} for configuration options
|
|
449
|
+
* @see {@link configureMiddleware} for registering middleware
|
|
450
|
+
*
|
|
451
|
+
* @public
|
|
452
|
+
* @since 0.6.0
|
|
453
|
+
*/
|
|
81
454
|
declare const createPerformanceMiddleware: (config?: PerformanceConfig) => Middleware;
|
|
82
455
|
|
|
83
456
|
/**
|
|
84
|
-
* Configuration for a blocker
|
|
457
|
+
* Configuration options for creating or updating a blocker.
|
|
458
|
+
*
|
|
459
|
+
* All fields are optional. Defaults will be applied for any missing values:
|
|
460
|
+
* - `scope`: "global"
|
|
461
|
+
* - `reason`: "Blocking UI"
|
|
462
|
+
* - `priority`: 50
|
|
463
|
+
* - `timestamp`: Current time
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* Basic configuration
|
|
467
|
+
* ```ts
|
|
468
|
+
* const config: BlockerConfig = {
|
|
469
|
+
* scope: 'form',
|
|
470
|
+
* reason: 'Submitting data...',
|
|
471
|
+
* priority: 70
|
|
472
|
+
* };
|
|
473
|
+
* ```
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* With timeout and callback
|
|
477
|
+
* ```ts
|
|
478
|
+
* const config: BlockerConfig = {
|
|
479
|
+
* scope: ['form', 'navigation'],
|
|
480
|
+
* reason: 'Critical operation in progress',
|
|
481
|
+
* priority: 90,
|
|
482
|
+
* timeout: 30000, // 30 seconds
|
|
483
|
+
* onTimeout: (id) => {
|
|
484
|
+
* console.warn(`Blocker ${id} timed out`);
|
|
485
|
+
* }
|
|
486
|
+
* };
|
|
487
|
+
* ```
|
|
488
|
+
*
|
|
489
|
+
* @public
|
|
490
|
+
* @since 0.6.0
|
|
85
491
|
*/
|
|
86
492
|
interface BlockerConfig {
|
|
87
493
|
scope?: string | ReadonlyArray<string>;
|
|
@@ -95,7 +501,16 @@ interface BlockerConfig {
|
|
|
95
501
|
onTimeout?: (blockerId: string) => void;
|
|
96
502
|
}
|
|
97
503
|
/**
|
|
98
|
-
*
|
|
504
|
+
* Internal representation of a blocker with all fields populated.
|
|
505
|
+
*
|
|
506
|
+
* This is the stored format in the Map. All optional fields from {@link BlockerConfig}
|
|
507
|
+
* are converted to required fields with default values applied.
|
|
508
|
+
*
|
|
509
|
+
* @internal This type is primarily for internal use. Users should work with
|
|
510
|
+
* {@link BlockerConfig} for configuration and {@link BlockerInfo} for
|
|
511
|
+
* reading blocker data.
|
|
512
|
+
*
|
|
513
|
+
* @since 0.6.0
|
|
99
514
|
*/
|
|
100
515
|
interface StoredBlocker {
|
|
101
516
|
scope: string | ReadonlyArray<string>;
|
|
@@ -110,20 +525,70 @@ interface StoredBlocker {
|
|
|
110
525
|
onTimeout?: (blockerId: string) => void;
|
|
111
526
|
}
|
|
112
527
|
/**
|
|
113
|
-
*
|
|
528
|
+
* Complete blocker information including its unique identifier.
|
|
529
|
+
*
|
|
530
|
+
* This type extends {@link StoredBlocker} with the blocker's ID, making it
|
|
531
|
+
* suitable for reading and displaying blocker information. Returned by
|
|
532
|
+
* {@link UIBlockingStoreActions.getBlockingInfo} and used by {@link useBlockingInfo}.
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* Accessing blocker info
|
|
536
|
+
* ```ts
|
|
537
|
+
* const blockers: BlockerInfo[] = store.getState().getBlockingInfo('form');
|
|
538
|
+
* blockers.forEach(blocker => {
|
|
539
|
+
* console.log(`${blocker.id}: ${blocker.reason} (priority: ${blocker.priority})`);
|
|
540
|
+
* });
|
|
541
|
+
* ```
|
|
542
|
+
*
|
|
543
|
+
* @public
|
|
544
|
+
* @since 0.6.0
|
|
114
545
|
*/
|
|
115
546
|
interface BlockerInfo extends StoredBlocker {
|
|
116
547
|
id: string;
|
|
117
548
|
}
|
|
118
549
|
/**
|
|
119
|
-
* Internal store
|
|
550
|
+
* Internal state structure of the UI blocking store.
|
|
551
|
+
*
|
|
552
|
+
* Contains the core data structures for managing blockers and middleware.
|
|
553
|
+
* This state is combined with {@link UIBlockingStoreActions} to create the
|
|
554
|
+
* complete {@link UIBlockingStore} type.
|
|
555
|
+
*
|
|
556
|
+
* @internal
|
|
557
|
+
* @since 0.6.0
|
|
120
558
|
*/
|
|
121
559
|
interface UIBlockingStoreState {
|
|
122
560
|
activeBlockers: Map<string, StoredBlocker>;
|
|
123
561
|
middlewares: Map<string, Middleware>;
|
|
124
562
|
}
|
|
125
563
|
/**
|
|
126
|
-
*
|
|
564
|
+
* All available actions for managing UI blocking state.
|
|
565
|
+
*
|
|
566
|
+
* These methods allow adding, removing, updating blockers, checking blocking status,
|
|
567
|
+
* and managing middleware. Combined with {@link UIBlockingStoreState} to form
|
|
568
|
+
* the complete {@link UIBlockingStore} type.
|
|
569
|
+
*
|
|
570
|
+
* All actions are safe to call multiple times and handle edge cases gracefully.
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* Using store actions directly
|
|
574
|
+
* ```ts
|
|
575
|
+
* import { uiBlockingStoreApi } from '@okyrychenko-dev/react-action-guard';
|
|
576
|
+
*
|
|
577
|
+
* // Add a blocker
|
|
578
|
+
* uiBlockingStoreApi.getState().addBlocker('my-blocker', {
|
|
579
|
+
* scope: 'form',
|
|
580
|
+
* reason: 'Processing...'
|
|
581
|
+
* });
|
|
582
|
+
*
|
|
583
|
+
* // Check if blocked
|
|
584
|
+
* const isBlocked = uiBlockingStoreApi.getState().isBlocked('form');
|
|
585
|
+
*
|
|
586
|
+
* // Remove blocker
|
|
587
|
+
* uiBlockingStoreApi.getState().removeBlocker('my-blocker');
|
|
588
|
+
* ```
|
|
589
|
+
*
|
|
590
|
+
* @public
|
|
591
|
+
* @since 0.6.0
|
|
127
592
|
*/
|
|
128
593
|
interface UIBlockingStoreActions {
|
|
129
594
|
addBlocker: (id: string, config?: BlockerConfig) => void;
|
|
@@ -138,7 +603,32 @@ interface UIBlockingStoreActions {
|
|
|
138
603
|
runMiddlewares: (context: MiddlewareContext) => Promise<void>;
|
|
139
604
|
}
|
|
140
605
|
/**
|
|
141
|
-
* Complete store type
|
|
606
|
+
* Complete UI blocking store type combining state and actions.
|
|
607
|
+
*
|
|
608
|
+
* This is the full store interface used by hooks like {@link useUIBlockingStore}
|
|
609
|
+
* and accessible via {@link uiBlockingStoreApi}. It combines all state properties
|
|
610
|
+
* from {@link UIBlockingStoreState} with all action methods from {@link UIBlockingStoreActions}.
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* Using the complete store
|
|
614
|
+
* ```ts
|
|
615
|
+
* import { useUIBlockingStore } from '@okyrychenko-dev/react-action-guard';
|
|
616
|
+
*
|
|
617
|
+
* function MyComponent() {
|
|
618
|
+
* const store = useUIBlockingStore();
|
|
619
|
+
*
|
|
620
|
+
* // Access state
|
|
621
|
+
* console.log(store.activeBlockers.size);
|
|
622
|
+
*
|
|
623
|
+
* // Call actions
|
|
624
|
+
* store.addBlocker('my-blocker', { scope: 'form' });
|
|
625
|
+
*
|
|
626
|
+
* return <div>...</div>;
|
|
627
|
+
* }
|
|
628
|
+
* ```
|
|
629
|
+
*
|
|
630
|
+
* @public
|
|
631
|
+
* @since 0.6.0
|
|
142
632
|
*/
|
|
143
633
|
type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
|
|
144
634
|
|
|
@@ -162,35 +652,157 @@ declare const useUIBlockingStore: {
|
|
|
162
652
|
declare const uiBlockingStoreApi: zustand.StoreApi<UIBlockingStore>;
|
|
163
653
|
|
|
164
654
|
/**
|
|
165
|
-
*
|
|
655
|
+
* Automatically manages a UI blocker based on component lifecycle.
|
|
656
|
+
*
|
|
657
|
+
* Creates a blocker that is automatically added when the condition is true
|
|
658
|
+
* and removed when the component unmounts or condition becomes false.
|
|
659
|
+
* Supports scoped blocking, priorities, timeouts, and works with both
|
|
660
|
+
* global store and isolated provider instances.
|
|
166
661
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
662
|
+
* @param blockerId - Unique identifier for this blocker. Must be unique within the scope.
|
|
663
|
+
* @param config - Configuration object for the blocker
|
|
664
|
+
* @param config.scope - Scope(s) to block. Can be a single scope string (e.g., "form", "navigation")
|
|
665
|
+
* or an array of scopes (e.g., ["form", "navigation"]). Defaults to "global".
|
|
666
|
+
* @param config.reason - Human-readable reason for blocking, displayed in DevTools and UI
|
|
667
|
+
* @param config.priority - Priority level (0-100). Higher priority blockers take precedence.
|
|
668
|
+
* Use 0-30 for low priority, 31-70 for medium, 71-100 for high priority.
|
|
669
|
+
* @param config.timeout - Auto-removal timeout in milliseconds. Blocker will be automatically
|
|
670
|
+
* removed after this duration. Use 0 or undefined for no timeout.
|
|
671
|
+
* @param config.onTimeout - Callback function invoked when the blocker times out
|
|
672
|
+
* @param isActive - When true, blocker is active; when false, blocker is removed.
|
|
673
|
+
* Defaults to true. Use this for conditional blocking.
|
|
169
674
|
*
|
|
170
|
-
*
|
|
675
|
+
* @example
|
|
676
|
+
* Basic form submission blocking
|
|
677
|
+
* ```tsx
|
|
678
|
+
* function MyForm() {
|
|
679
|
+
* const [isSubmitting, setIsSubmitting] = useState(false);
|
|
680
|
+
*
|
|
681
|
+
* useBlocker('form-submit', {
|
|
682
|
+
* scope: 'form',
|
|
683
|
+
* reason: 'Submitting form...',
|
|
684
|
+
* priority: 50,
|
|
685
|
+
* timeout: 30000, // Auto-remove after 30 seconds
|
|
686
|
+
* onTimeout: () => {
|
|
687
|
+
* console.warn('Form submission timed out');
|
|
688
|
+
* setIsSubmitting(false);
|
|
689
|
+
* }
|
|
690
|
+
* }, isSubmitting);
|
|
691
|
+
*
|
|
692
|
+
* return <form onSubmit={handleSubmit}>...</form>;
|
|
693
|
+
* }
|
|
694
|
+
* ```
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* Multiple scopes with high priority
|
|
698
|
+
* ```tsx
|
|
699
|
+
* useBlocker('critical-save', {
|
|
700
|
+
* scope: ['form', 'navigation', 'actions'],
|
|
701
|
+
* reason: 'Saving critical data...',
|
|
702
|
+
* priority: 90,
|
|
703
|
+
* }, isSaving);
|
|
704
|
+
* ```
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
* Conditional blocking based on unsaved changes
|
|
708
|
+
* ```tsx
|
|
709
|
+
* function Editor() {
|
|
710
|
+
* const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
711
|
+
*
|
|
712
|
+
* useBlocker('unsaved-changes', {
|
|
713
|
+
* scope: 'navigation',
|
|
714
|
+
* reason: 'You have unsaved changes',
|
|
715
|
+
* priority: 80,
|
|
716
|
+
* }, hasUnsavedChanges);
|
|
717
|
+
*
|
|
718
|
+
* return <div>...</div>;
|
|
719
|
+
* }
|
|
720
|
+
* ```
|
|
171
721
|
*
|
|
172
|
-
* @
|
|
173
|
-
* @
|
|
174
|
-
* @
|
|
722
|
+
* @see {@link useIsBlocked} to check if a scope is currently blocked
|
|
723
|
+
* @see {@link useBlockingInfo} to get detailed blocker information
|
|
724
|
+
* @see {@link useAsyncAction} for async operation wrapping with automatic blocking
|
|
725
|
+
* @see {@link useConfirmableBlocker} for blockers that require user confirmation
|
|
175
726
|
*
|
|
727
|
+
* @public
|
|
728
|
+
* @since 0.6.0
|
|
176
729
|
*/
|
|
177
730
|
declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?: boolean) => void;
|
|
178
731
|
|
|
179
732
|
/**
|
|
180
|
-
*
|
|
733
|
+
* Gets detailed information about all active blockers for a specific scope.
|
|
181
734
|
*
|
|
182
|
-
* Returns an array of blocker information objects
|
|
183
|
-
*
|
|
735
|
+
* Returns an array of blocker information objects, sorted by priority (highest first).
|
|
736
|
+
* Each blocker object contains id, reason, priority, and scope information.
|
|
737
|
+
* The hook automatically re-renders when blockers are added, removed, or updated.
|
|
184
738
|
*
|
|
185
|
-
* Uses a stable reference - only re-renders when the actual
|
|
186
|
-
* not on every store update.
|
|
739
|
+
* Uses memoization to provide a stable reference - only re-renders when the actual
|
|
740
|
+
* blocker list changes, not on every store update. This makes it efficient for
|
|
741
|
+
* rendering lists of blockers.
|
|
187
742
|
*
|
|
188
|
-
*
|
|
743
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
189
744
|
*
|
|
190
|
-
* @param scope - Scope to get blocking information for
|
|
191
|
-
* @returns Array of
|
|
745
|
+
* @param scope - Scope to get blocking information for. Defaults to "global" if not specified.
|
|
746
|
+
* @returns Array of {@link BlockerInfo} objects, sorted by priority (highest to lowest)
|
|
192
747
|
*
|
|
748
|
+
* @example
|
|
749
|
+
* Display list of active blockers
|
|
750
|
+
* ```tsx
|
|
751
|
+
* function BlockerList() {
|
|
752
|
+
* const blockers = useBlockingInfo('form');
|
|
753
|
+
*
|
|
754
|
+
* if (blockers.length === 0) {
|
|
755
|
+
* return <div>No active blockers</div>;
|
|
756
|
+
* }
|
|
757
|
+
*
|
|
758
|
+
* return (
|
|
759
|
+
* <ul>
|
|
760
|
+
* {blockers.map((blocker) => (
|
|
761
|
+
* <li key={blocker.id}>
|
|
762
|
+
* {blocker.reason} (Priority: {blocker.priority})
|
|
763
|
+
* </li>
|
|
764
|
+
* ))}
|
|
765
|
+
* </ul>
|
|
766
|
+
* );
|
|
767
|
+
* }
|
|
768
|
+
* ```
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* Show highest priority blocker reason
|
|
772
|
+
* ```tsx
|
|
773
|
+
* function BlockingBanner() {
|
|
774
|
+
* const blockers = useBlockingInfo('global');
|
|
775
|
+
*
|
|
776
|
+
* // blockers are already sorted by priority (highest first)
|
|
777
|
+
* const topBlocker = blockers[0];
|
|
778
|
+
*
|
|
779
|
+
* if (!topBlocker) {
|
|
780
|
+
* return null;
|
|
781
|
+
* }
|
|
782
|
+
*
|
|
783
|
+
* return <div className="banner">{topBlocker.reason}</div>;
|
|
784
|
+
* }
|
|
193
785
|
* ```
|
|
786
|
+
*
|
|
787
|
+
* @example
|
|
788
|
+
* Count active blockers
|
|
789
|
+
* ```tsx
|
|
790
|
+
* function BlockerCount() {
|
|
791
|
+
* const formBlockers = useBlockingInfo('form');
|
|
792
|
+
* const navBlockers = useBlockingInfo('navigation');
|
|
793
|
+
*
|
|
794
|
+
* const total = formBlockers.length + navBlockers.length;
|
|
795
|
+
*
|
|
796
|
+
* return <div>{total} active blockers</div>;
|
|
797
|
+
* }
|
|
798
|
+
* ```
|
|
799
|
+
*
|
|
800
|
+
* @see {@link useIsBlocked} for a simple boolean check
|
|
801
|
+
* @see {@link useBlocker} to create blockers
|
|
802
|
+
* @see {@link BlockerInfo} for the structure of blocker information objects
|
|
803
|
+
*
|
|
804
|
+
* @public
|
|
805
|
+
* @since 0.6.0
|
|
194
806
|
*/
|
|
195
807
|
declare const useBlockingInfo: (scope?: string) => ReadonlyArray<BlockerInfo>;
|
|
196
808
|
|
|
@@ -212,16 +824,150 @@ interface ConditionalBlockerConfig<TState = unknown> extends Omit<BlockerConfig,
|
|
|
212
824
|
}
|
|
213
825
|
|
|
214
826
|
/**
|
|
215
|
-
*
|
|
827
|
+
* Blocks UI based on a dynamic condition that is periodically evaluated.
|
|
828
|
+
*
|
|
829
|
+
* Periodically checks a condition function and automatically adds or removes
|
|
830
|
+
* a blocker based on the result. Useful for blocking based on application state,
|
|
831
|
+
* network status, feature flags, or any other dynamic condition.
|
|
216
832
|
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
833
|
+
* The condition function receives optional state and returns a boolean.
|
|
834
|
+
* It's evaluated immediately on mount and then at the specified interval.
|
|
835
|
+
* The blocker is added/removed automatically as the condition changes.
|
|
219
836
|
*
|
|
220
|
-
*
|
|
837
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
221
838
|
*
|
|
222
|
-
* @
|
|
223
|
-
* @param
|
|
839
|
+
* @template TState - Type of the state object passed to the condition function
|
|
840
|
+
* @param blockerId - Unique identifier for this conditional blocker
|
|
841
|
+
* @param config - Configuration for conditional blocking
|
|
842
|
+
* @param config.condition - Function that returns true to block, false to unblock.
|
|
843
|
+
* Receives the `state` object as parameter.
|
|
844
|
+
* @param config.state - Optional state object passed to the condition function
|
|
845
|
+
* @param config.checkInterval - How often to check the condition in milliseconds.
|
|
846
|
+
* Defaults to 1000ms (1 second). Lower values mean
|
|
847
|
+
* more responsive blocking but higher CPU usage.
|
|
848
|
+
* @param config.scope - Scope(s) to block when condition is true
|
|
849
|
+
* @param config.reason - Reason for blocking (displayed to users)
|
|
850
|
+
* @param config.priority - Optional blocker priority (0-100)
|
|
224
851
|
*
|
|
852
|
+
* @example
|
|
853
|
+
* Block based on network status
|
|
854
|
+
* ```tsx
|
|
855
|
+
* function OfflineBlocker() {
|
|
856
|
+
* const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
857
|
+
*
|
|
858
|
+
* useEffect(() => {
|
|
859
|
+
* const handleOnline = () => setIsOnline(true);
|
|
860
|
+
* const handleOffline = () => setIsOnline(false);
|
|
861
|
+
*
|
|
862
|
+
* window.addEventListener('online', handleOnline);
|
|
863
|
+
* window.addEventListener('offline', handleOffline);
|
|
864
|
+
*
|
|
865
|
+
* return () => {
|
|
866
|
+
* window.removeEventListener('online', handleOnline);
|
|
867
|
+
* window.removeEventListener('offline', handleOffline);
|
|
868
|
+
* };
|
|
869
|
+
* }, []);
|
|
870
|
+
*
|
|
871
|
+
* useConditionalBlocker('offline', {
|
|
872
|
+
* condition: () => !isOnline,
|
|
873
|
+
* state: { isOnline },
|
|
874
|
+
* checkInterval: 500, // Check every 500ms
|
|
875
|
+
* scope: 'global',
|
|
876
|
+
* reason: 'No internet connection. Please check your network.',
|
|
877
|
+
* priority: 95
|
|
878
|
+
* });
|
|
879
|
+
*
|
|
880
|
+
* return <App />;
|
|
881
|
+
* }
|
|
882
|
+
* ```
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* Block based on feature flag
|
|
886
|
+
* ```tsx
|
|
887
|
+
* function FeatureFlagBlocker() {
|
|
888
|
+
* const featureFlags = useFeatureFlags();
|
|
889
|
+
*
|
|
890
|
+
* useConditionalBlocker('feature-disabled', {
|
|
891
|
+
* condition: (state) => !state.featureFlags.newFeatureEnabled,
|
|
892
|
+
* state: { featureFlags },
|
|
893
|
+
* checkInterval: 5000, // Check every 5 seconds
|
|
894
|
+
* scope: 'new-feature',
|
|
895
|
+
* reason: 'This feature is currently disabled',
|
|
896
|
+
* priority: 70
|
|
897
|
+
* });
|
|
898
|
+
*
|
|
899
|
+
* return <NewFeature />;
|
|
900
|
+
* }
|
|
901
|
+
* ```
|
|
902
|
+
*
|
|
903
|
+
* @example
|
|
904
|
+
* Block during API rate limit
|
|
905
|
+
* ```tsx
|
|
906
|
+
* function RateLimitBlocker() {
|
|
907
|
+
* const [requestCount, setRequestCount] = useState(0);
|
|
908
|
+
* const [resetTime, setResetTime] = useState<number | null>(null);
|
|
909
|
+
*
|
|
910
|
+
* useConditionalBlocker('rate-limit', {
|
|
911
|
+
* condition: (state) => {
|
|
912
|
+
* if (!state.resetTime) {
|
|
913
|
+
* return false;
|
|
914
|
+
* }
|
|
915
|
+
* return Date.now() < state.resetTime && state.requestCount >= 100;
|
|
916
|
+
* },
|
|
917
|
+
* state: { requestCount, resetTime },
|
|
918
|
+
* checkInterval: 1000,
|
|
919
|
+
* scope: 'api',
|
|
920
|
+
* reason: 'API rate limit reached. Please wait.',
|
|
921
|
+
* priority: 90
|
|
922
|
+
* });
|
|
923
|
+
*
|
|
924
|
+
* return <Dashboard />;
|
|
925
|
+
* }
|
|
926
|
+
* ```
|
|
927
|
+
*
|
|
928
|
+
* @example
|
|
929
|
+
* Block based on form validation
|
|
930
|
+
* ```tsx
|
|
931
|
+
* function FormValidationBlocker() {
|
|
932
|
+
* const { formState } = useFormContext();
|
|
933
|
+
*
|
|
934
|
+
* useConditionalBlocker('invalid-form', {
|
|
935
|
+
* condition: (state) => !state.isValid && state.isDirty,
|
|
936
|
+
* state: formState,
|
|
937
|
+
* checkInterval: 500,
|
|
938
|
+
* scope: 'form-submit',
|
|
939
|
+
* reason: 'Please fix form errors before submitting',
|
|
940
|
+
* priority: 60
|
|
941
|
+
* });
|
|
942
|
+
*
|
|
943
|
+
* return <Form />;
|
|
944
|
+
* }
|
|
945
|
+
* ```
|
|
946
|
+
*
|
|
947
|
+
* @example
|
|
948
|
+
* Without state parameter (simple condition)
|
|
949
|
+
* ```tsx
|
|
950
|
+
* useConditionalBlocker('low-battery', {
|
|
951
|
+
* condition: () => {
|
|
952
|
+
* // Check battery level if available
|
|
953
|
+
* if ('getBattery' in navigator) {
|
|
954
|
+
* return navigator.getBattery().then(b => b.level < 0.05);
|
|
955
|
+
* }
|
|
956
|
+
* return false;
|
|
957
|
+
* },
|
|
958
|
+
* checkInterval: 30000, // Check every 30 seconds
|
|
959
|
+
* scope: 'heavy-operations',
|
|
960
|
+
* reason: 'Battery level too low for this operation',
|
|
961
|
+
* priority: 80
|
|
962
|
+
* });
|
|
963
|
+
* ```
|
|
964
|
+
*
|
|
965
|
+
* @see {@link useBlocker} for simple conditional blocking with boolean
|
|
966
|
+
* @see {@link useScheduledBlocker} for time-based blocking
|
|
967
|
+
* @see {@link ConditionalBlockerConfig} for configuration options
|
|
968
|
+
*
|
|
969
|
+
* @public
|
|
970
|
+
* @since 0.6.0
|
|
225
971
|
*/
|
|
226
972
|
declare const useConditionalBlocker: <TState = unknown>(blockerId: string, config: ConditionalBlockerConfig<TState>) => void;
|
|
227
973
|
|
|
@@ -267,22 +1013,211 @@ interface UseConfirmableBlockerReturn {
|
|
|
267
1013
|
}
|
|
268
1014
|
|
|
269
1015
|
/**
|
|
270
|
-
*
|
|
1016
|
+
* Creates a blocker that requires user confirmation before executing an action.
|
|
1017
|
+
*
|
|
1018
|
+
* Manages a confirmation dialog and blocking state for actions that need user approval.
|
|
1019
|
+
* The blocker is active during both the confirmation dialog display and action execution.
|
|
1020
|
+
*
|
|
1021
|
+
* Workflow:
|
|
1022
|
+
* 1. Call `execute()` to show confirmation dialog (blocker activated)
|
|
1023
|
+
* 2. User confirms → `onConfirm` callback runs (blocker remains active)
|
|
1024
|
+
* 3. Action completes → blocker removed
|
|
1025
|
+
* 4. User cancels → blocker removed, optional `onCancel` callback runs
|
|
1026
|
+
*
|
|
1027
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
1028
|
+
*
|
|
1029
|
+
* @param blockerId - Unique identifier for this confirmable blocker
|
|
1030
|
+
* @param config - Configuration for the confirmable action
|
|
1031
|
+
* @param config.confirmMessage - Message to display in the confirmation dialog
|
|
1032
|
+
* @param config.confirmTitle - Optional dialog title (defaults to "Confirm Action")
|
|
1033
|
+
* @param config.confirmButtonText - Optional confirm button text (defaults to "Confirm")
|
|
1034
|
+
* @param config.cancelButtonText - Optional cancel button text (defaults to "Cancel")
|
|
1035
|
+
* @param config.onConfirm - Async function to execute after user confirms
|
|
1036
|
+
* @param config.onCancel - Optional callback when user cancels
|
|
1037
|
+
* @param config.scope - Scope(s) to block during confirmation and execution
|
|
1038
|
+
* @param config.reason - Optional custom blocking reason (defaults to confirmMessage)
|
|
1039
|
+
* @param config.priority - Optional blocker priority (0-100)
|
|
1040
|
+
*
|
|
1041
|
+
* @returns Object containing:
|
|
1042
|
+
* - `execute`: Function to trigger the confirmation dialog
|
|
1043
|
+
* - `isDialogOpen`: Boolean indicating if confirmation dialog is visible
|
|
1044
|
+
* - `isExecuting`: Boolean indicating if the confirmed action is executing
|
|
1045
|
+
* - `confirmConfig`: Dialog configuration (title, message, button texts)
|
|
1046
|
+
* - `onConfirm`: Function to call when user confirms
|
|
1047
|
+
* - `onCancel`: Function to call when user cancels
|
|
1048
|
+
*
|
|
1049
|
+
* @example
|
|
1050
|
+
* Delete confirmation with custom dialog
|
|
1051
|
+
* ```tsx
|
|
1052
|
+
* function DeleteButton({ itemId, itemName }) {
|
|
1053
|
+
* const confirmableDelete = useConfirmableBlocker('delete-item', {
|
|
1054
|
+
* confirmMessage: `Are you sure you want to delete "${itemName}"?`,
|
|
1055
|
+
* confirmTitle: 'Delete Item',
|
|
1056
|
+
* confirmButtonText: 'Delete',
|
|
1057
|
+
* cancelButtonText: 'Keep',
|
|
1058
|
+
* scope: ['form', 'navigation'],
|
|
1059
|
+
* priority: 80,
|
|
1060
|
+
* onConfirm: async () => {
|
|
1061
|
+
* await deleteItem(itemId);
|
|
1062
|
+
* showToast('Item deleted successfully');
|
|
1063
|
+
* },
|
|
1064
|
+
* onCancel: () => {
|
|
1065
|
+
* console.log('Delete cancelled');
|
|
1066
|
+
* }
|
|
1067
|
+
* });
|
|
271
1068
|
*
|
|
1069
|
+
* return (
|
|
1070
|
+
* <>
|
|
1071
|
+
* <button onClick={confirmableDelete.execute}>
|
|
1072
|
+
* Delete
|
|
1073
|
+
* </button>
|
|
1074
|
+
*
|
|
1075
|
+
* {confirmableDelete.isDialogOpen && (
|
|
1076
|
+
* <ConfirmDialog
|
|
1077
|
+
* {...confirmableDelete.confirmConfig}
|
|
1078
|
+
* onConfirm={confirmableDelete.onConfirm}
|
|
1079
|
+
* onCancel={confirmableDelete.onCancel}
|
|
1080
|
+
* isLoading={confirmableDelete.isExecuting}
|
|
1081
|
+
* />
|
|
1082
|
+
* )}
|
|
1083
|
+
* </>
|
|
1084
|
+
* );
|
|
1085
|
+
* }
|
|
1086
|
+
* ```
|
|
1087
|
+
*
|
|
1088
|
+
* @example
|
|
1089
|
+
* Simple confirmation with default dialog
|
|
1090
|
+
* ```tsx
|
|
1091
|
+
* function LogoutButton() {
|
|
1092
|
+
* const confirmLogout = useConfirmableBlocker('logout', {
|
|
1093
|
+
* confirmMessage: 'Are you sure you want to log out?',
|
|
1094
|
+
* scope: 'global',
|
|
1095
|
+
* onConfirm: async () => {
|
|
1096
|
+
* await api.logout();
|
|
1097
|
+
* navigate('/login');
|
|
1098
|
+
* }
|
|
1099
|
+
* });
|
|
1100
|
+
*
|
|
1101
|
+
* return (
|
|
1102
|
+
* <>
|
|
1103
|
+
* <button onClick={confirmLogout.execute}>Logout</button>
|
|
1104
|
+
*
|
|
1105
|
+
* {confirmLogout.isDialogOpen && (
|
|
1106
|
+
* <Dialog
|
|
1107
|
+
* title={confirmLogout.confirmConfig.title}
|
|
1108
|
+
* message={confirmLogout.confirmConfig.message}
|
|
1109
|
+
* onConfirm={confirmLogout.onConfirm}
|
|
1110
|
+
* onCancel={confirmLogout.onCancel}
|
|
1111
|
+
* />
|
|
1112
|
+
* )}
|
|
1113
|
+
* </>
|
|
1114
|
+
* );
|
|
1115
|
+
* }
|
|
1116
|
+
* ```
|
|
1117
|
+
*
|
|
1118
|
+
* @example
|
|
1119
|
+
* With loading state during execution
|
|
1120
|
+
* ```tsx
|
|
1121
|
+
* function SaveButton() {
|
|
1122
|
+
* const confirmSave = useConfirmableBlocker('save-changes', {
|
|
1123
|
+
* confirmMessage: 'Save all changes?',
|
|
1124
|
+
* scope: 'form',
|
|
1125
|
+
* onConfirm: async () => {
|
|
1126
|
+
* await saveAllChanges();
|
|
1127
|
+
* }
|
|
1128
|
+
* });
|
|
1129
|
+
*
|
|
1130
|
+
* return (
|
|
1131
|
+
* <button
|
|
1132
|
+
* onClick={confirmSave.execute}
|
|
1133
|
+
* disabled={confirmSave.isExecuting}
|
|
1134
|
+
* >
|
|
1135
|
+
* {confirmSave.isExecuting ? 'Saving...' : 'Save All'}
|
|
1136
|
+
* </button>
|
|
1137
|
+
* );
|
|
1138
|
+
* }
|
|
1139
|
+
* ```
|
|
1140
|
+
*
|
|
1141
|
+
* @see {@link useBlocker} for simple blocking without confirmation
|
|
1142
|
+
* @see {@link ConfirmableBlockerConfig} for configuration options
|
|
1143
|
+
* @see {@link UseConfirmableBlockerReturn} for return value structure
|
|
1144
|
+
*
|
|
1145
|
+
* @public
|
|
1146
|
+
* @since 0.6.0
|
|
272
1147
|
*/
|
|
273
1148
|
declare const useConfirmableBlocker: (blockerId: string, config: ConfirmableBlockerConfig) => UseConfirmableBlockerReturn;
|
|
274
1149
|
|
|
275
1150
|
/**
|
|
276
|
-
*
|
|
1151
|
+
* Checks if one or more scopes are currently blocked.
|
|
1152
|
+
*
|
|
1153
|
+
* Returns a boolean indicating whether any of the specified scopes have active blockers.
|
|
1154
|
+
* The hook automatically re-renders the component when the blocking state changes,
|
|
1155
|
+
* ensuring your UI always reflects the current blocking state.
|
|
1156
|
+
*
|
|
1157
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
1158
|
+
*
|
|
1159
|
+
* @param scope - Scope(s) to check for blocking. Can be:
|
|
1160
|
+
* - A single scope string (e.g., "form", "navigation", "global")
|
|
1161
|
+
* - An array of scopes (e.g., ["form", "navigation"])
|
|
1162
|
+
* - undefined to check the "global" scope
|
|
1163
|
+
* @returns true if ANY of the specified scopes are blocked, false otherwise
|
|
1164
|
+
*
|
|
1165
|
+
* @example
|
|
1166
|
+
* Basic single scope check
|
|
1167
|
+
* ```tsx
|
|
1168
|
+
* function SubmitButton() {
|
|
1169
|
+
* const isFormBlocked = useIsBlocked('form');
|
|
277
1170
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
1171
|
+
* return (
|
|
1172
|
+
* <button disabled={isFormBlocked}>
|
|
1173
|
+
* {isFormBlocked ? 'Processing...' : 'Submit'}
|
|
1174
|
+
* </button>
|
|
1175
|
+
* );
|
|
1176
|
+
* }
|
|
1177
|
+
* ```
|
|
280
1178
|
*
|
|
281
|
-
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* Multiple scopes check
|
|
1181
|
+
* ```tsx
|
|
1182
|
+
* function NavigationLink({ href, children }) {
|
|
1183
|
+
* const isBlocked = useIsBlocked(['navigation', 'global']);
|
|
1184
|
+
*
|
|
1185
|
+
* return (
|
|
1186
|
+
* <a
|
|
1187
|
+
* href={href}
|
|
1188
|
+
* onClick={(e) => {
|
|
1189
|
+
* if (isBlocked) {
|
|
1190
|
+
* e.preventDefault();
|
|
1191
|
+
* alert('Navigation is currently blocked');
|
|
1192
|
+
* }
|
|
1193
|
+
* }}
|
|
1194
|
+
* style={{ opacity: isBlocked ? 0.5 : 1 }}
|
|
1195
|
+
* >
|
|
1196
|
+
* {children}
|
|
1197
|
+
* </a>
|
|
1198
|
+
* );
|
|
1199
|
+
* }
|
|
1200
|
+
* ```
|
|
282
1201
|
*
|
|
283
|
-
* @
|
|
284
|
-
*
|
|
1202
|
+
* @example
|
|
1203
|
+
* Conditional rendering based on blocking state
|
|
1204
|
+
* ```tsx
|
|
1205
|
+
* function SaveIndicator() {
|
|
1206
|
+
* const isSaving = useIsBlocked('save');
|
|
1207
|
+
*
|
|
1208
|
+
* if (!isSaving) {
|
|
1209
|
+
* return null;
|
|
1210
|
+
* }
|
|
1211
|
+
*
|
|
1212
|
+
* return <div className="spinner">Saving...</div>;
|
|
1213
|
+
* }
|
|
1214
|
+
* ```
|
|
285
1215
|
*
|
|
1216
|
+
* @see {@link useBlocker} to create a blocker
|
|
1217
|
+
* @see {@link useBlockingInfo} to get detailed information about active blockers
|
|
1218
|
+
*
|
|
1219
|
+
* @public
|
|
1220
|
+
* @since 0.6.0
|
|
286
1221
|
*/
|
|
287
1222
|
declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
288
1223
|
|
|
@@ -316,17 +1251,125 @@ interface ScheduledBlockerConfig extends BlockerConfig {
|
|
|
316
1251
|
}
|
|
317
1252
|
|
|
318
1253
|
/**
|
|
319
|
-
*
|
|
1254
|
+
* Schedules UI blocking for a specific time period (e.g., maintenance windows).
|
|
1255
|
+
*
|
|
1256
|
+
* Automatically blocks UI between specified start and end times. Useful for
|
|
1257
|
+
* scheduled maintenance, planned downtime, or time-based feature restrictions.
|
|
1258
|
+
* Supports callbacks when the blocking period starts and ends.
|
|
1259
|
+
*
|
|
1260
|
+
* Time specifications support:
|
|
1261
|
+
* - Date objects
|
|
1262
|
+
* - ISO 8601 strings (e.g., "2024-01-15T10:00:00Z")
|
|
1263
|
+
* - Unix timestamps (milliseconds since epoch)
|
|
1264
|
+
*
|
|
1265
|
+
* You can specify the end time either as an absolute time or as a duration
|
|
1266
|
+
* from the start time. If both are provided, duration takes precedence.
|
|
1267
|
+
*
|
|
1268
|
+
* Works with both the global store and isolated provider instances (via UIBlockingProvider).
|
|
1269
|
+
*
|
|
1270
|
+
* @param blockerId - Unique identifier for this scheduled blocker
|
|
1271
|
+
* @param config - Schedule configuration
|
|
1272
|
+
* @param config.schedule - Schedule timing configuration
|
|
1273
|
+
* @param config.schedule.start - When to start blocking (Date, ISO string, or timestamp)
|
|
1274
|
+
* @param config.schedule.end - Optional absolute end time (Date, ISO string, or timestamp)
|
|
1275
|
+
* @param config.schedule.duration - Optional duration in milliseconds from start time
|
|
1276
|
+
* @param config.scope - Scope(s) to block during the scheduled period
|
|
1277
|
+
* @param config.reason - Reason for blocking (displayed to users)
|
|
1278
|
+
* @param config.priority - Optional blocker priority (0-100)
|
|
1279
|
+
* @param config.onScheduleStart - Optional callback when blocking period starts
|
|
1280
|
+
* @param config.onScheduleEnd - Optional callback when blocking period ends
|
|
1281
|
+
*
|
|
1282
|
+
* @example
|
|
1283
|
+
* Maintenance window with absolute times
|
|
1284
|
+
* ```tsx
|
|
1285
|
+
* function MaintenanceWarning() {
|
|
1286
|
+
* useScheduledBlocker('maintenance', {
|
|
1287
|
+
* schedule: {
|
|
1288
|
+
* start: new Date('2024-01-15T02:00:00Z'),
|
|
1289
|
+
* end: new Date('2024-01-15T04:00:00Z'),
|
|
1290
|
+
* },
|
|
1291
|
+
* scope: 'global',
|
|
1292
|
+
* reason: 'System maintenance in progress',
|
|
1293
|
+
* priority: 95,
|
|
1294
|
+
* onScheduleStart: () => {
|
|
1295
|
+
* console.log('Maintenance started');
|
|
1296
|
+
* showNotification('System is now in maintenance mode');
|
|
1297
|
+
* },
|
|
1298
|
+
* onScheduleEnd: () => {
|
|
1299
|
+
* console.log('Maintenance completed');
|
|
1300
|
+
* showNotification('System is back online');
|
|
1301
|
+
* }
|
|
1302
|
+
* });
|
|
320
1303
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
1304
|
+
* return <div>...</div>;
|
|
1305
|
+
* }
|
|
1306
|
+
* ```
|
|
324
1307
|
*
|
|
325
|
-
*
|
|
1308
|
+
* @example
|
|
1309
|
+
* Short maintenance with duration
|
|
1310
|
+
* ```tsx
|
|
1311
|
+
* useScheduledBlocker('quick-maintenance', {
|
|
1312
|
+
* schedule: {
|
|
1313
|
+
* start: new Date('2024-01-15T03:00:00'),
|
|
1314
|
+
* duration: 15 * 60 * 1000, // 15 minutes
|
|
1315
|
+
* },
|
|
1316
|
+
* scope: ['api', 'forms'],
|
|
1317
|
+
* reason: 'Brief system update',
|
|
1318
|
+
* priority: 90
|
|
1319
|
+
* });
|
|
1320
|
+
* ```
|
|
1321
|
+
*
|
|
1322
|
+
* @example
|
|
1323
|
+
* Business hours enforcement
|
|
1324
|
+
* ```tsx
|
|
1325
|
+
* function BusinessHoursBlocker() {
|
|
1326
|
+
* const now = new Date();
|
|
1327
|
+
* const tomorrow9AM = new Date(now);
|
|
1328
|
+
* tomorrow9AM.setDate(now.getDate() + 1);
|
|
1329
|
+
* tomorrow9AM.setHours(9, 0, 0, 0);
|
|
326
1330
|
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
1331
|
+
* useScheduledBlocker('after-hours', {
|
|
1332
|
+
* schedule: {
|
|
1333
|
+
* start: new Date(now.getTime() + 1000), // Start immediately
|
|
1334
|
+
* end: tomorrow9AM,
|
|
1335
|
+
* },
|
|
1336
|
+
* scope: 'trading',
|
|
1337
|
+
* reason: 'Trading is only available during business hours (9 AM - 5 PM)',
|
|
1338
|
+
* priority: 85
|
|
1339
|
+
* });
|
|
329
1340
|
*
|
|
1341
|
+
* return <TradingDashboard />;
|
|
1342
|
+
* }
|
|
1343
|
+
* ```
|
|
1344
|
+
*
|
|
1345
|
+
* @example
|
|
1346
|
+
* Using ISO strings for scheduling
|
|
1347
|
+
* ```tsx
|
|
1348
|
+
* useScheduledBlocker('deployment', {
|
|
1349
|
+
* schedule: {
|
|
1350
|
+
* start: '2024-01-20T00:00:00Z',
|
|
1351
|
+
* duration: 2 * 60 * 60 * 1000, // 2 hours
|
|
1352
|
+
* },
|
|
1353
|
+
* scope: 'global',
|
|
1354
|
+
* reason: 'Deploying new version',
|
|
1355
|
+
* priority: 100,
|
|
1356
|
+
* onScheduleStart: () => {
|
|
1357
|
+
* analytics.track('deployment_started');
|
|
1358
|
+
* },
|
|
1359
|
+
* onScheduleEnd: () => {
|
|
1360
|
+
* analytics.track('deployment_completed');
|
|
1361
|
+
* window.location.reload(); // Reload to get new version
|
|
1362
|
+
* }
|
|
1363
|
+
* });
|
|
1364
|
+
* ```
|
|
1365
|
+
*
|
|
1366
|
+
* @see {@link useBlocker} for immediate blocking without scheduling
|
|
1367
|
+
* @see {@link useConditionalBlocker} for condition-based blocking
|
|
1368
|
+
* @see {@link ScheduledBlockerConfig} for configuration options
|
|
1369
|
+
* @see {@link BlockingSchedule} for schedule specification details
|
|
1370
|
+
*
|
|
1371
|
+
* @public
|
|
1372
|
+
* @since 0.6.0
|
|
330
1373
|
*/
|
|
331
1374
|
declare const useScheduledBlocker: (blockerId: string, config: ScheduledBlockerConfig) => void;
|
|
332
1375
|
|
|
@@ -399,7 +1442,14 @@ interface TypedHooks<TScope extends string> {
|
|
|
399
1442
|
declare function createTypedHooks<TScope extends string>(): TypedHooks<TScope>;
|
|
400
1443
|
|
|
401
1444
|
/**
|
|
402
|
-
* Props for UIBlockingProvider component
|
|
1445
|
+
* Props for the {@link UIBlockingProvider} component.
|
|
1446
|
+
*
|
|
1447
|
+
* Configures an isolated UI blocking store instance for a subtree of your app.
|
|
1448
|
+
* Useful for SSR, testing, micro-frontends, or any scenario where you need
|
|
1449
|
+
* independent blocking state.
|
|
1450
|
+
*
|
|
1451
|
+
* @public
|
|
1452
|
+
* @since 0.6.0
|
|
403
1453
|
*/
|
|
404
1454
|
interface UIBlockingProviderProps {
|
|
405
1455
|
children: ReactNode;
|
|
@@ -411,8 +1461,8 @@ interface UIBlockingProviderProps {
|
|
|
411
1461
|
middlewares?: ReadonlyArray<Middleware>;
|
|
412
1462
|
}
|
|
413
1463
|
/**
|
|
414
|
-
* Base provider created using toolkit's createStoreProvider
|
|
415
|
-
*
|
|
1464
|
+
* Base provider implementation created using toolkit's createStoreProvider.
|
|
1465
|
+
* @internal
|
|
416
1466
|
*/
|
|
417
1467
|
declare const useUIBlockingContext: () => StoreApi<UIBlockingStore>;
|
|
418
1468
|
declare const useUIBlockingStoreFromContext: {
|
|
@@ -421,10 +1471,100 @@ declare const useUIBlockingStoreFromContext: {
|
|
|
421
1471
|
};
|
|
422
1472
|
declare const useIsInsideUIBlockingProvider: () => boolean;
|
|
423
1473
|
/**
|
|
424
|
-
* Provider component for isolated UI blocking state
|
|
1474
|
+
* Provider component for isolated UI blocking state management.
|
|
1475
|
+
*
|
|
1476
|
+
* Creates an independent store instance for a subtree of your application.
|
|
1477
|
+
* Each provider has its own isolated blocking state, separate from the global
|
|
1478
|
+
* store and other provider instances.
|
|
1479
|
+
*
|
|
1480
|
+
* **Use cases:**
|
|
1481
|
+
* - Server-Side Rendering (SSR) - Avoid state sharing between requests
|
|
1482
|
+
* - Testing - Isolated state for each test
|
|
1483
|
+
* - Micro-frontends - Independent blocking state per micro-app
|
|
1484
|
+
* - Modals/Dialogs - Scoped blocking within a dialog
|
|
1485
|
+
*
|
|
1486
|
+
* Uses `createStoreProvider` from `@okyrychenko-dev/react-zustand-toolkit` with
|
|
1487
|
+
* built-in DevTools support and middleware registration.
|
|
1488
|
+
*
|
|
1489
|
+
* @param props - Provider configuration
|
|
1490
|
+
* @param props.children - React children to wrap with the provider
|
|
1491
|
+
* @param props.enableDevtools - Enable Redux DevTools integration. Defaults to true in development, false in production.
|
|
1492
|
+
* @param props.devtoolsName - Name displayed in Redux DevTools. Defaults to "UIBlocking".
|
|
1493
|
+
* @param props.middlewares - Array of middleware functions to register on store creation
|
|
1494
|
+
*
|
|
1495
|
+
* @example
|
|
1496
|
+
* Basic SSR setup
|
|
1497
|
+
* ```tsx
|
|
1498
|
+
* import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
|
|
1499
|
+
*
|
|
1500
|
+
* function App() {
|
|
1501
|
+
* return (
|
|
1502
|
+
* <UIBlockingProvider>
|
|
1503
|
+
* <MyApplication />
|
|
1504
|
+
* </UIBlockingProvider>
|
|
1505
|
+
* );
|
|
1506
|
+
* }
|
|
1507
|
+
* ```
|
|
1508
|
+
*
|
|
1509
|
+
* @example
|
|
1510
|
+
* With middleware and custom DevTools name
|
|
1511
|
+
* ```tsx
|
|
1512
|
+
* import { UIBlockingProvider, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
|
|
1513
|
+
*
|
|
1514
|
+
* function App() {
|
|
1515
|
+
* return (
|
|
1516
|
+
* <UIBlockingProvider
|
|
1517
|
+
* enableDevtools={true}
|
|
1518
|
+
* devtoolsName="MyApp-UIBlocking"
|
|
1519
|
+
* middlewares={[loggerMiddleware]}
|
|
1520
|
+
* >
|
|
1521
|
+
* <MyApplication />
|
|
1522
|
+
* </UIBlockingProvider>
|
|
1523
|
+
* );
|
|
1524
|
+
* }
|
|
1525
|
+
* ```
|
|
1526
|
+
*
|
|
1527
|
+
* @example
|
|
1528
|
+
* Multiple providers for micro-frontends
|
|
1529
|
+
* ```tsx
|
|
1530
|
+
* function MicroFrontendApp() {
|
|
1531
|
+
* return (
|
|
1532
|
+
* <div>
|
|
1533
|
+
* <UIBlockingProvider devtoolsName="MicroApp1">
|
|
1534
|
+
* <MicroApp1 />
|
|
1535
|
+
* </UIBlockingProvider>
|
|
1536
|
+
*
|
|
1537
|
+
* <UIBlockingProvider devtoolsName="MicroApp2">
|
|
1538
|
+
* <MicroApp2 />
|
|
1539
|
+
* </UIBlockingProvider>
|
|
1540
|
+
* </div>
|
|
1541
|
+
* );
|
|
1542
|
+
* }
|
|
1543
|
+
* ```
|
|
1544
|
+
*
|
|
1545
|
+
* @example
|
|
1546
|
+
* Testing with isolated state
|
|
1547
|
+
* ```tsx
|
|
1548
|
+
* // In your test file
|
|
1549
|
+
* import { render } from '@testing-library/react';
|
|
1550
|
+
* import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
|
|
1551
|
+
*
|
|
1552
|
+
* test('blocker behavior', () => {
|
|
1553
|
+
* render(
|
|
1554
|
+
* <UIBlockingProvider enableDevtools={false}>
|
|
1555
|
+
* <ComponentUnderTest />
|
|
1556
|
+
* </UIBlockingProvider>
|
|
1557
|
+
* );
|
|
1558
|
+
* // Each test gets isolated state
|
|
1559
|
+
* });
|
|
1560
|
+
* ```
|
|
1561
|
+
*
|
|
1562
|
+
* @see {@link useUIBlockingContext} to access the store from context
|
|
1563
|
+
* @see {@link useIsInsideUIBlockingProvider} to check if inside a provider
|
|
1564
|
+
* @see {@link UIBlockingProviderProps} for prop details
|
|
425
1565
|
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
1566
|
+
* @public
|
|
1567
|
+
* @since 0.6.0
|
|
428
1568
|
*/
|
|
429
1569
|
declare function UIBlockingProvider({ children, enableDevtools, devtoolsName, middlewares, }: UIBlockingProviderProps): ReactNode;
|
|
430
1570
|
|