@trackunit/react-modal 1.20.10 → 1.20.11
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/index.cjs.js +251 -237
- package/index.esm.js +252 -238
- package/package.json +3 -2
- package/src/modal/Modal.d.ts +1 -1
- package/src/modal/useModal.d.ts +10 -0
package/index.cjs.js
CHANGED
|
@@ -266,238 +266,6 @@ const useModalFooterBorder = (rootRef, { footerClass = "border-t", enabled = tru
|
|
|
266
266
|
}, [enabled, footerClass, bodySelector, footerSelector, rootRef]);
|
|
267
267
|
};
|
|
268
268
|
|
|
269
|
-
/**
|
|
270
|
-
* Modal Stack Registry - Cross-Bundle Modal Awareness
|
|
271
|
-
*
|
|
272
|
-
* ## Architecture Overview
|
|
273
|
-
*
|
|
274
|
-
* This module provides a registry for tracking open modals across bundle boundaries
|
|
275
|
-
* (host application + Iris app iframes). It enables proper modal stacking behavior
|
|
276
|
-
* where modals can visually indicate their depth (scale, backdrop visibility, animations).
|
|
277
|
-
*
|
|
278
|
-
* ## Why postMessage Instead of Penpal?
|
|
279
|
-
*
|
|
280
|
-
* While most host-iframe communication uses Penpal (RPC-style), modal stack changes
|
|
281
|
-
* use raw postMessage for these reasons:
|
|
282
|
-
*
|
|
283
|
-
* 1. **Package Independence**: This module is part of `@trackunit/react-modal`, a standalone
|
|
284
|
-
* UI component library. It should not depend on `iris-app-runtime` to avoid circular
|
|
285
|
-
* dependencies and keep the modal component reusable.
|
|
286
|
-
*
|
|
287
|
-
* 2. **Broadcast Semantics**: When a host modal opens, ALL iframe modals need to know
|
|
288
|
-
* simultaneously (not just one specific connection). postMessage with broadcast
|
|
289
|
-
* to all iframes fits this pattern naturally.
|
|
290
|
-
*
|
|
291
|
-
* 3. **Low Latency**: Stack count changes need real-time sync for smooth animations
|
|
292
|
-
* (scale transforms, backdrop fading). Raw postMessage has less overhead than Penpal.
|
|
293
|
-
*
|
|
294
|
-
* 4. **Module Isolation**: Each bundle (host + each iframe) gets its own instance of
|
|
295
|
-
* this registry. They need cross-window synchronization, not shared state.
|
|
296
|
-
*
|
|
297
|
-
* ## Communication Flow
|
|
298
|
-
*
|
|
299
|
-
* ```
|
|
300
|
-
* ┌─────────────────────────────────────────────────────────────────────┐
|
|
301
|
-
* │ HOST APPLICATION │
|
|
302
|
-
* │ │
|
|
303
|
-
* │ modalStackRegistry ←───────────────────────────────────────────┐ │
|
|
304
|
-
* │ │ │ │
|
|
305
|
-
* │ │ subscribe() │ │
|
|
306
|
-
* │ ▼ │ │
|
|
307
|
-
* │ ModalDialogProviderHost │ │
|
|
308
|
-
* │ │ │ │
|
|
309
|
-
* │ ├─► broadcasts MODAL_STACK_MESSAGE_TYPE to all iframes │ │
|
|
310
|
-
* │ │ │ │
|
|
311
|
-
* │ └─► listens for IFRAME_MODAL_STACK_MESSAGE_TYPE ──────────┘ │
|
|
312
|
-
* │ (aggregates per-iframe, cleans up on iframe removal) │
|
|
313
|
-
* └─────────────────────────────────────────────────────────────────────┘
|
|
314
|
-
* │
|
|
315
|
-
* postMessage (bidirectional)
|
|
316
|
-
* │
|
|
317
|
-
* ┌─────────────────────────────────────────────────────────────────────┐
|
|
318
|
-
* │ IFRAME (Iris App) │
|
|
319
|
-
* │ │
|
|
320
|
-
* │ modalStackRegistry (separate instance) │
|
|
321
|
-
* │ │ │
|
|
322
|
-
* │ ├─► listens for MODAL_STACK_MESSAGE_TYPE from host │
|
|
323
|
-
* │ │ (updates hostModalCount) │
|
|
324
|
-
* │ │ │
|
|
325
|
-
* │ └─► on register/unregister, broadcasts │
|
|
326
|
-
* │ IFRAME_MODAL_STACK_MESSAGE_TYPE to parent │
|
|
327
|
-
* └─────────────────────────────────────────────────────────────────────┘
|
|
328
|
-
* ```
|
|
329
|
-
*
|
|
330
|
-
* ## Usage with useModalStack Hook
|
|
331
|
-
*
|
|
332
|
-
* The `useModalStack` hook consumes this registry to calculate:
|
|
333
|
-
* - `depthFromFront`: How many modals are in front of this one (0 = frontmost)
|
|
334
|
-
* - `stackSize`: Total modals open (local + host + iframe)
|
|
335
|
-
* - `stackSizeAtOpen`: Stack size when this modal opened (for animation decisions)
|
|
336
|
-
*
|
|
337
|
-
* This enables the Modal component to:
|
|
338
|
-
* - Scale down non-frontmost modals (visual stacking effect)
|
|
339
|
-
* - Show backdrop only on the frontmost modal
|
|
340
|
-
* - Choose appropriate animations based on whether it's opening over another modal
|
|
341
|
-
*
|
|
342
|
-
* @module modalStackRegistry
|
|
343
|
-
*/
|
|
344
|
-
let modalStack = [];
|
|
345
|
-
let hostModalCount = 0;
|
|
346
|
-
let iframeModalCount = 0;
|
|
347
|
-
const listeners = new Set();
|
|
348
|
-
const notify = () => listeners.forEach(listener => listener());
|
|
349
|
-
/**
|
|
350
|
-
* Message type for host → iframe modal stack communication.
|
|
351
|
-
* The host broadcasts this to all iframes when its modal count changes.
|
|
352
|
-
* Iframes listen for this to update their `hostModalCount`.
|
|
353
|
-
*/
|
|
354
|
-
const MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_HOST_MODAL_STACK_CHANGE";
|
|
355
|
-
/**
|
|
356
|
-
* Message type for iframe → host modal stack communication.
|
|
357
|
-
* Each iframe sends this to the parent when its modal count changes.
|
|
358
|
-
* The host aggregates these per-iframe for accurate total tracking.
|
|
359
|
-
*/
|
|
360
|
-
const IFRAME_MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_IFRAME_MODAL_STACK_CHANGE";
|
|
361
|
-
const isInIframe = typeof window !== "undefined" && window.parent !== window;
|
|
362
|
-
/**
|
|
363
|
-
* Broadcast this iframe's modal count to the parent (host).
|
|
364
|
-
* Only runs when in an iframe context. Called automatically on register/unregister.
|
|
365
|
-
*/
|
|
366
|
-
const broadcastToParent = (modalCount) => {
|
|
367
|
-
if (isInIframe) {
|
|
368
|
-
window.parent.postMessage({ type: IFRAME_MODAL_STACK_MESSAGE_TYPE, data: { iframeModalCount: modalCount } }, "*");
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
/**
|
|
372
|
-
* Set up the global message listener for cross-bundle modal stack communication.
|
|
373
|
-
* This listener runs in the same bundle context as the Modal components, ensuring
|
|
374
|
-
* the correct registry instance is updated.
|
|
375
|
-
*
|
|
376
|
-
* Note: The host also has a separate listener in ModalDialogProviderHost that
|
|
377
|
-
* tracks per-iframe counts and handles cleanup when iframes are removed.
|
|
378
|
-
*/
|
|
379
|
-
if (typeof window !== "undefined") {
|
|
380
|
-
window.addEventListener("message", event => {
|
|
381
|
-
// Host → Iframe: Update host modal count
|
|
382
|
-
if (event.data?.type === MODAL_STACK_MESSAGE_TYPE && typeof event.data?.data?.hostModalCount === "number") {
|
|
383
|
-
modalStackRegistry.setHostModalCount(event.data.data.hostModalCount);
|
|
384
|
-
}
|
|
385
|
-
// Iframe → Host: Update iframe modal count (fallback, primary handler is in ModalDialogProviderHost)
|
|
386
|
-
if (event.data?.type === IFRAME_MODAL_STACK_MESSAGE_TYPE &&
|
|
387
|
-
typeof event.data?.data?.iframeModalCount === "number") {
|
|
388
|
-
modalStackRegistry.setIframeModalCount(event.data.data.iframeModalCount);
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Module-level registry to track the stack of open modals.
|
|
394
|
-
*
|
|
395
|
-
* This registry enables:
|
|
396
|
-
* - Tracking modal order within a single bundle (local stack)
|
|
397
|
-
* - Awareness of modals in other bundles (host ↔ iframe communication)
|
|
398
|
-
* - Subscriber notifications for React integration via useSyncExternalStore
|
|
399
|
-
*
|
|
400
|
-
* Cross-bundle counts:
|
|
401
|
-
* - `hostModalCount`: Number of modals open in the host (relevant for iframes)
|
|
402
|
-
* - `iframeModalCount`: Number of modals open in iframes (relevant for host)
|
|
403
|
-
*
|
|
404
|
-
* Note: This is intentionally NOT a React hook because:
|
|
405
|
-
* 1. It needs to be a singleton that persists across React component lifecycles
|
|
406
|
-
* 2. It must work with postMessage for cross-bundle communication (host ↔ iframe)
|
|
407
|
-
* 3. The useModalStack hook consumes this registry via useSyncExternalStore for React integration
|
|
408
|
-
*/
|
|
409
|
-
const modalStackRegistry = {
|
|
410
|
-
register: (id) => {
|
|
411
|
-
modalStack.push(id);
|
|
412
|
-
notify();
|
|
413
|
-
// If in iframe, notify parent of our modal count
|
|
414
|
-
broadcastToParent(modalStack.length);
|
|
415
|
-
},
|
|
416
|
-
unregister: (id) => {
|
|
417
|
-
modalStack = modalStack.filter(modalId => modalId !== id);
|
|
418
|
-
notify();
|
|
419
|
-
// If in iframe, notify parent of our modal count
|
|
420
|
-
broadcastToParent(modalStack.length);
|
|
421
|
-
},
|
|
422
|
-
getStackPosition: (id) => modalStack.indexOf(id),
|
|
423
|
-
getStackSize: () => modalStack.length,
|
|
424
|
-
/** Get the number of modals open in the host (only relevant for Iris apps in iframes) */
|
|
425
|
-
getHostModalCount: () => hostModalCount,
|
|
426
|
-
/** Get the number of modals open in iframes (only relevant for host) */
|
|
427
|
-
getIframeModalCount: () => iframeModalCount,
|
|
428
|
-
/** Set the host modal count (called when receiving messages from host) */
|
|
429
|
-
setHostModalCount: (count) => {
|
|
430
|
-
if (hostModalCount !== count) {
|
|
431
|
-
hostModalCount = count;
|
|
432
|
-
notify();
|
|
433
|
-
}
|
|
434
|
-
},
|
|
435
|
-
/** Set the iframe modal count (called when receiving messages from iframes) */
|
|
436
|
-
setIframeModalCount: (count) => {
|
|
437
|
-
if (iframeModalCount !== count) {
|
|
438
|
-
iframeModalCount = count;
|
|
439
|
-
notify();
|
|
440
|
-
}
|
|
441
|
-
},
|
|
442
|
-
subscribe: (listener) => {
|
|
443
|
-
listeners.add(listener);
|
|
444
|
-
return () => {
|
|
445
|
-
listeners.delete(listener);
|
|
446
|
-
};
|
|
447
|
-
},
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
// Track the stack size when each modal opened (includes host modals for proper animation)
|
|
451
|
-
const modalOpenState = new Map();
|
|
452
|
-
/**
|
|
453
|
-
* Hook to track this modal's position in the stack of open modals.
|
|
454
|
-
* Returns the depth from front (0 = frontmost, 1 = one modal in front, etc.)
|
|
455
|
-
*
|
|
456
|
-
* This hook is aware of modals across bundle boundaries (host + Iris apps)
|
|
457
|
-
* by listening for host modal count changes via postMessage.
|
|
458
|
-
*
|
|
459
|
-
* @param isOpen - Whether the modal is currently open
|
|
460
|
-
*/
|
|
461
|
-
const useModalStack = (isOpen) => {
|
|
462
|
-
const modalId = react.useId();
|
|
463
|
-
// Subscribe to stack changes to re-render when stack updates
|
|
464
|
-
const localStackSize = react.useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getStackSize, modalStackRegistry.getStackSize);
|
|
465
|
-
// Subscribe to host modal count (only relevant for Iris apps in iframes)
|
|
466
|
-
const hostModalCount = react.useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getHostModalCount, modalStackRegistry.getHostModalCount);
|
|
467
|
-
// Subscribe to iframe modal count (only relevant for host)
|
|
468
|
-
const iframeModalCount = react.useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getIframeModalCount, modalStackRegistry.getIframeModalCount);
|
|
469
|
-
const stackPosition = react.useSyncExternalStore(modalStackRegistry.subscribe, () => modalStackRegistry.getStackPosition(modalId), () => modalStackRegistry.getStackPosition(modalId));
|
|
470
|
-
// Register/unregister based on open state
|
|
471
|
-
// useLayoutEffect ensures registration happens before paint to avoid backdrop flash
|
|
472
|
-
react.useLayoutEffect(() => {
|
|
473
|
-
if (isOpen) {
|
|
474
|
-
// Capture total stack size before registering (includes cross-bundle modals for proper animation)
|
|
475
|
-
// - hostModalCount: modals in host (relevant for iframes)
|
|
476
|
-
// - iframeModalCount: modals in iframes (relevant for host)
|
|
477
|
-
const totalSizeAtOpen = modalStackRegistry.getStackSize() +
|
|
478
|
-
modalStackRegistry.getHostModalCount() +
|
|
479
|
-
modalStackRegistry.getIframeModalCount();
|
|
480
|
-
modalOpenState.set(modalId, totalSizeAtOpen);
|
|
481
|
-
modalStackRegistry.register(modalId);
|
|
482
|
-
return () => {
|
|
483
|
-
modalStackRegistry.unregister(modalId);
|
|
484
|
-
modalOpenState.delete(modalId);
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
return undefined;
|
|
488
|
-
}, [isOpen, modalId]);
|
|
489
|
-
// Total stack size includes local modals, host modals, and iframe modals
|
|
490
|
-
const totalStackSize = localStackSize + hostModalCount + iframeModalCount;
|
|
491
|
-
// Calculate depth from front (0 = frontmost)
|
|
492
|
-
// Host modals are always "on top" of Iris app modals, so if hostModalCount > 0,
|
|
493
|
-
// all Iris app modals have at least that many modals in front of them
|
|
494
|
-
const localDepthFromFront = stackPosition >= 0 ? localStackSize - 1 - stackPosition : 0;
|
|
495
|
-
const depthFromFront = localDepthFromFront + hostModalCount;
|
|
496
|
-
// Get the total stack size when this modal opened (stable once set)
|
|
497
|
-
const stackSizeAtOpen = modalOpenState.get(modalId) ?? 0;
|
|
498
|
-
return react.useMemo(() => ({ depthFromFront, stackSize: totalStackSize, stackSizeAtOpen }), [depthFromFront, totalStackSize, stackSizeAtOpen]);
|
|
499
|
-
};
|
|
500
|
-
|
|
501
269
|
/** Scale factor per modal depth level (5% smaller per level back) */
|
|
502
270
|
const SCALE_FACTOR_PER_LEVEL = 0.05;
|
|
503
271
|
/**
|
|
@@ -535,7 +303,7 @@ const SCALE_FACTOR_PER_LEVEL = 0.05;
|
|
|
535
303
|
* @param {ModalProps} props - The props for the Modal component
|
|
536
304
|
* @returns {ReactElement} Modal component
|
|
537
305
|
*/
|
|
538
|
-
const Modal = ({ children, isOpen, role = "dialog", "data-testid": dataTestId, className, size, floatingUi, ref, restoreFocus = true, }) => {
|
|
306
|
+
const Modal = ({ children, isOpen, role = "dialog", "data-testid": dataTestId, className, size, floatingUi, ref, restoreFocus = true, depthFromFront, stackSizeAtOpen, }) => {
|
|
539
307
|
// For dialogs/modals, Floating UI recommends not using floatingStyles since the modal
|
|
540
308
|
// is viewport-centered via CSS, not positioned relative to a reference element.
|
|
541
309
|
// See: https://floating-ui.com/docs/dialog
|
|
@@ -544,8 +312,6 @@ const Modal = ({ children, isOpen, role = "dialog", "data-testid": dataTestId, c
|
|
|
544
312
|
// Merge the custom ref from useModal with FloatingUI's setFloating ref
|
|
545
313
|
const mergedRef = react$1.useMergeRefs([refs.setFloating, ref]);
|
|
546
314
|
useModalFooterBorder(cardRef, { enabled: isOpen, footerClass: "border-t pt-4" });
|
|
547
|
-
// Track modal stack position for stacked modal styling
|
|
548
|
-
const { depthFromFront, stackSizeAtOpen } = useModalStack(isOpen);
|
|
549
315
|
const isFrontmost = depthFromFront === 0;
|
|
550
316
|
return (jsxRuntime.jsx(reactComponents.Portal, { root: rootElement, children: isOpen ? (jsxRuntime.jsx(react$1.FloatingOverlay, { className: cvaModalBackdrop({ isFrontmost, shouldAnimate: stackSizeAtOpen === 0 }), lockScroll: true, children: jsxRuntime.jsx(react$1.FloatingFocusManager, { context: context, restoreFocus: restoreFocus, children: jsxRuntime.jsx("div", { "aria-modal": true, className: cvaModalContainer(), ref: mergedRef, role: role,
|
|
551
317
|
// Scale down modals that are behind the frontmost
|
|
@@ -763,6 +529,238 @@ const ModalHeader = react.forwardRef(({ heading, subHeading, onClickClose, "data
|
|
|
763
529
|
}), "data-testid": dataTestId, id: id, ref: ref, children: [jsxRuntime.jsxs("div", { className: cvaHeadingContainer(), children: [jsxRuntime.jsxs("div", { className: cvaTitleContainer(), children: [jsxRuntime.jsx(reactComponents.Heading, { variant: "tertiary", children: heading }), accessories] }), Boolean(subHeading) ? (jsxRuntime.jsx(reactComponents.Text, { size: "small", subtle: true, children: subHeading })) : null, children] }), jsxRuntime.jsx("div", { className: cvaIconContainer(), children: jsxRuntime.jsx(reactComponents.IconButton, { className: "!h-min", "data-testid": dataTestId ? `${dataTestId}-close-button` : "modal-close-button", icon: jsxRuntime.jsx(reactComponents.Icon, { name: "XMark", size: "small" }), onClick: onClickClose, size: "small", variant: "ghost-neutral" }) })] }));
|
|
764
530
|
});
|
|
765
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Modal Stack Registry - Cross-Bundle Modal Awareness
|
|
534
|
+
*
|
|
535
|
+
* ## Architecture Overview
|
|
536
|
+
*
|
|
537
|
+
* This module provides a registry for tracking open modals across bundle boundaries
|
|
538
|
+
* (host application + Iris app iframes). It enables proper modal stacking behavior
|
|
539
|
+
* where modals can visually indicate their depth (scale, backdrop visibility, animations).
|
|
540
|
+
*
|
|
541
|
+
* ## Why postMessage Instead of Penpal?
|
|
542
|
+
*
|
|
543
|
+
* While most host-iframe communication uses Penpal (RPC-style), modal stack changes
|
|
544
|
+
* use raw postMessage for these reasons:
|
|
545
|
+
*
|
|
546
|
+
* 1. **Package Independence**: This module is part of `@trackunit/react-modal`, a standalone
|
|
547
|
+
* UI component library. It should not depend on `iris-app-runtime` to avoid circular
|
|
548
|
+
* dependencies and keep the modal component reusable.
|
|
549
|
+
*
|
|
550
|
+
* 2. **Broadcast Semantics**: When a host modal opens, ALL iframe modals need to know
|
|
551
|
+
* simultaneously (not just one specific connection). postMessage with broadcast
|
|
552
|
+
* to all iframes fits this pattern naturally.
|
|
553
|
+
*
|
|
554
|
+
* 3. **Low Latency**: Stack count changes need real-time sync for smooth animations
|
|
555
|
+
* (scale transforms, backdrop fading). Raw postMessage has less overhead than Penpal.
|
|
556
|
+
*
|
|
557
|
+
* 4. **Module Isolation**: Each bundle (host + each iframe) gets its own instance of
|
|
558
|
+
* this registry. They need cross-window synchronization, not shared state.
|
|
559
|
+
*
|
|
560
|
+
* ## Communication Flow
|
|
561
|
+
*
|
|
562
|
+
* ```
|
|
563
|
+
* ┌─────────────────────────────────────────────────────────────────────┐
|
|
564
|
+
* │ HOST APPLICATION │
|
|
565
|
+
* │ │
|
|
566
|
+
* │ modalStackRegistry ←───────────────────────────────────────────┐ │
|
|
567
|
+
* │ │ │ │
|
|
568
|
+
* │ │ subscribe() │ │
|
|
569
|
+
* │ ▼ │ │
|
|
570
|
+
* │ ModalDialogProviderHost │ │
|
|
571
|
+
* │ │ │ │
|
|
572
|
+
* │ ├─► broadcasts MODAL_STACK_MESSAGE_TYPE to all iframes │ │
|
|
573
|
+
* │ │ │ │
|
|
574
|
+
* │ └─► listens for IFRAME_MODAL_STACK_MESSAGE_TYPE ──────────┘ │
|
|
575
|
+
* │ (aggregates per-iframe, cleans up on iframe removal) │
|
|
576
|
+
* └─────────────────────────────────────────────────────────────────────┘
|
|
577
|
+
* │
|
|
578
|
+
* postMessage (bidirectional)
|
|
579
|
+
* │
|
|
580
|
+
* ┌─────────────────────────────────────────────────────────────────────┐
|
|
581
|
+
* │ IFRAME (Iris App) │
|
|
582
|
+
* │ │
|
|
583
|
+
* │ modalStackRegistry (separate instance) │
|
|
584
|
+
* │ │ │
|
|
585
|
+
* │ ├─► listens for MODAL_STACK_MESSAGE_TYPE from host │
|
|
586
|
+
* │ │ (updates hostModalCount) │
|
|
587
|
+
* │ │ │
|
|
588
|
+
* │ └─► on register/unregister, broadcasts │
|
|
589
|
+
* │ IFRAME_MODAL_STACK_MESSAGE_TYPE to parent │
|
|
590
|
+
* └─────────────────────────────────────────────────────────────────────┘
|
|
591
|
+
* ```
|
|
592
|
+
*
|
|
593
|
+
* ## Usage with useModalStack Hook
|
|
594
|
+
*
|
|
595
|
+
* The `useModalStack` hook consumes this registry to calculate:
|
|
596
|
+
* - `depthFromFront`: How many modals are in front of this one (0 = frontmost)
|
|
597
|
+
* - `stackSize`: Total modals open (local + host + iframe)
|
|
598
|
+
* - `stackSizeAtOpen`: Stack size when this modal opened (for animation decisions)
|
|
599
|
+
*
|
|
600
|
+
* This enables the Modal component to:
|
|
601
|
+
* - Scale down non-frontmost modals (visual stacking effect)
|
|
602
|
+
* - Show backdrop only on the frontmost modal
|
|
603
|
+
* - Choose appropriate animations based on whether it's opening over another modal
|
|
604
|
+
*
|
|
605
|
+
* @module modalStackRegistry
|
|
606
|
+
*/
|
|
607
|
+
let modalStack = [];
|
|
608
|
+
let hostModalCount = 0;
|
|
609
|
+
let iframeModalCount = 0;
|
|
610
|
+
const listeners = new Set();
|
|
611
|
+
const notify = () => listeners.forEach(listener => listener());
|
|
612
|
+
/**
|
|
613
|
+
* Message type for host → iframe modal stack communication.
|
|
614
|
+
* The host broadcasts this to all iframes when its modal count changes.
|
|
615
|
+
* Iframes listen for this to update their `hostModalCount`.
|
|
616
|
+
*/
|
|
617
|
+
const MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_HOST_MODAL_STACK_CHANGE";
|
|
618
|
+
/**
|
|
619
|
+
* Message type for iframe → host modal stack communication.
|
|
620
|
+
* Each iframe sends this to the parent when its modal count changes.
|
|
621
|
+
* The host aggregates these per-iframe for accurate total tracking.
|
|
622
|
+
*/
|
|
623
|
+
const IFRAME_MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_IFRAME_MODAL_STACK_CHANGE";
|
|
624
|
+
const isInIframe = typeof window !== "undefined" && window.parent !== window;
|
|
625
|
+
/**
|
|
626
|
+
* Broadcast this iframe's modal count to the parent (host).
|
|
627
|
+
* Only runs when in an iframe context. Called automatically on register/unregister.
|
|
628
|
+
*/
|
|
629
|
+
const broadcastToParent = (modalCount) => {
|
|
630
|
+
if (isInIframe) {
|
|
631
|
+
window.parent.postMessage({ type: IFRAME_MODAL_STACK_MESSAGE_TYPE, data: { iframeModalCount: modalCount } }, "*");
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
/**
|
|
635
|
+
* Set up the global message listener for cross-bundle modal stack communication.
|
|
636
|
+
* This listener runs in the same bundle context as the Modal components, ensuring
|
|
637
|
+
* the correct registry instance is updated.
|
|
638
|
+
*
|
|
639
|
+
* Note: The host also has a separate listener in ModalDialogProviderHost that
|
|
640
|
+
* tracks per-iframe counts and handles cleanup when iframes are removed.
|
|
641
|
+
*/
|
|
642
|
+
if (typeof window !== "undefined") {
|
|
643
|
+
window.addEventListener("message", event => {
|
|
644
|
+
// Host → Iframe: Update host modal count
|
|
645
|
+
if (event.data?.type === MODAL_STACK_MESSAGE_TYPE && typeof event.data?.data?.hostModalCount === "number") {
|
|
646
|
+
modalStackRegistry.setHostModalCount(event.data.data.hostModalCount);
|
|
647
|
+
}
|
|
648
|
+
// Iframe → Host: Update iframe modal count (fallback, primary handler is in ModalDialogProviderHost)
|
|
649
|
+
if (event.data?.type === IFRAME_MODAL_STACK_MESSAGE_TYPE &&
|
|
650
|
+
typeof event.data?.data?.iframeModalCount === "number") {
|
|
651
|
+
modalStackRegistry.setIframeModalCount(event.data.data.iframeModalCount);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Module-level registry to track the stack of open modals.
|
|
657
|
+
*
|
|
658
|
+
* This registry enables:
|
|
659
|
+
* - Tracking modal order within a single bundle (local stack)
|
|
660
|
+
* - Awareness of modals in other bundles (host ↔ iframe communication)
|
|
661
|
+
* - Subscriber notifications for React integration via useSyncExternalStore
|
|
662
|
+
*
|
|
663
|
+
* Cross-bundle counts:
|
|
664
|
+
* - `hostModalCount`: Number of modals open in the host (relevant for iframes)
|
|
665
|
+
* - `iframeModalCount`: Number of modals open in iframes (relevant for host)
|
|
666
|
+
*
|
|
667
|
+
* Note: This is intentionally NOT a React hook because:
|
|
668
|
+
* 1. It needs to be a singleton that persists across React component lifecycles
|
|
669
|
+
* 2. It must work with postMessage for cross-bundle communication (host ↔ iframe)
|
|
670
|
+
* 3. The useModalStack hook consumes this registry via useSyncExternalStore for React integration
|
|
671
|
+
*/
|
|
672
|
+
const modalStackRegistry = {
|
|
673
|
+
register: (id) => {
|
|
674
|
+
modalStack.push(id);
|
|
675
|
+
notify();
|
|
676
|
+
// If in iframe, notify parent of our modal count
|
|
677
|
+
broadcastToParent(modalStack.length);
|
|
678
|
+
},
|
|
679
|
+
unregister: (id) => {
|
|
680
|
+
modalStack = modalStack.filter(modalId => modalId !== id);
|
|
681
|
+
notify();
|
|
682
|
+
// If in iframe, notify parent of our modal count
|
|
683
|
+
broadcastToParent(modalStack.length);
|
|
684
|
+
},
|
|
685
|
+
getStackPosition: (id) => modalStack.indexOf(id),
|
|
686
|
+
getStackSize: () => modalStack.length,
|
|
687
|
+
/** Get the number of modals open in the host (only relevant for Iris apps in iframes) */
|
|
688
|
+
getHostModalCount: () => hostModalCount,
|
|
689
|
+
/** Get the number of modals open in iframes (only relevant for host) */
|
|
690
|
+
getIframeModalCount: () => iframeModalCount,
|
|
691
|
+
/** Set the host modal count (called when receiving messages from host) */
|
|
692
|
+
setHostModalCount: (count) => {
|
|
693
|
+
if (hostModalCount !== count) {
|
|
694
|
+
hostModalCount = count;
|
|
695
|
+
notify();
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
/** Set the iframe modal count (called when receiving messages from iframes) */
|
|
699
|
+
setIframeModalCount: (count) => {
|
|
700
|
+
if (iframeModalCount !== count) {
|
|
701
|
+
iframeModalCount = count;
|
|
702
|
+
notify();
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
subscribe: (listener) => {
|
|
706
|
+
listeners.add(listener);
|
|
707
|
+
return () => {
|
|
708
|
+
listeners.delete(listener);
|
|
709
|
+
};
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
// Track the stack size when each modal opened (includes host modals for proper animation)
|
|
714
|
+
const modalOpenState = new Map();
|
|
715
|
+
/**
|
|
716
|
+
* Hook to track this modal's position in the stack of open modals.
|
|
717
|
+
* Returns the depth from front (0 = frontmost, 1 = one modal in front, etc.)
|
|
718
|
+
*
|
|
719
|
+
* This hook is aware of modals across bundle boundaries (host + Iris apps)
|
|
720
|
+
* by listening for host modal count changes via postMessage.
|
|
721
|
+
*
|
|
722
|
+
* @param isOpen - Whether the modal is currently open
|
|
723
|
+
*/
|
|
724
|
+
const useModalStack = (isOpen) => {
|
|
725
|
+
const modalId = react.useId();
|
|
726
|
+
// Subscribe to stack changes to re-render when stack updates
|
|
727
|
+
const localStackSize = react.useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getStackSize, modalStackRegistry.getStackSize);
|
|
728
|
+
// Subscribe to host modal count (only relevant for Iris apps in iframes)
|
|
729
|
+
const hostModalCount = react.useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getHostModalCount, modalStackRegistry.getHostModalCount);
|
|
730
|
+
// Subscribe to iframe modal count (only relevant for host)
|
|
731
|
+
const iframeModalCount = react.useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getIframeModalCount, modalStackRegistry.getIframeModalCount);
|
|
732
|
+
const stackPosition = react.useSyncExternalStore(modalStackRegistry.subscribe, () => modalStackRegistry.getStackPosition(modalId), () => modalStackRegistry.getStackPosition(modalId));
|
|
733
|
+
// Register/unregister based on open state
|
|
734
|
+
// useLayoutEffect ensures registration happens before paint to avoid backdrop flash
|
|
735
|
+
react.useLayoutEffect(() => {
|
|
736
|
+
if (isOpen) {
|
|
737
|
+
// Capture total stack size before registering (includes cross-bundle modals for proper animation)
|
|
738
|
+
// - hostModalCount: modals in host (relevant for iframes)
|
|
739
|
+
// - iframeModalCount: modals in iframes (relevant for host)
|
|
740
|
+
const totalSizeAtOpen = modalStackRegistry.getStackSize() +
|
|
741
|
+
modalStackRegistry.getHostModalCount() +
|
|
742
|
+
modalStackRegistry.getIframeModalCount();
|
|
743
|
+
modalOpenState.set(modalId, totalSizeAtOpen);
|
|
744
|
+
modalStackRegistry.register(modalId);
|
|
745
|
+
return () => {
|
|
746
|
+
modalStackRegistry.unregister(modalId);
|
|
747
|
+
modalOpenState.delete(modalId);
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
return undefined;
|
|
751
|
+
}, [isOpen, modalId]);
|
|
752
|
+
// Total stack size includes local modals, host modals, and iframe modals
|
|
753
|
+
const totalStackSize = localStackSize + hostModalCount + iframeModalCount;
|
|
754
|
+
// Calculate depth from front (0 = frontmost)
|
|
755
|
+
// Host modals are always "on top" of Iris app modals, so if hostModalCount > 0,
|
|
756
|
+
// all Iris app modals have at least that many modals in front of them
|
|
757
|
+
const localDepthFromFront = stackPosition >= 0 ? localStackSize - 1 - stackPosition : 0;
|
|
758
|
+
const depthFromFront = localDepthFromFront + hostModalCount;
|
|
759
|
+
// Get the total stack size when this modal opened (stable once set)
|
|
760
|
+
const stackSizeAtOpen = modalOpenState.get(modalId) ?? 0;
|
|
761
|
+
return react.useMemo(() => ({ depthFromFront, stackSize: totalStackSize, stackSizeAtOpen }), [depthFromFront, totalStackSize, stackSizeAtOpen]);
|
|
762
|
+
};
|
|
763
|
+
|
|
766
764
|
/**
|
|
767
765
|
* A hook to handle the state and configuration of Modal components.
|
|
768
766
|
*
|
|
@@ -858,7 +856,8 @@ const useModal = (props) => {
|
|
|
858
856
|
whileElementsMounted: react$1.autoUpdate,
|
|
859
857
|
middleware: [react$1.shift()],
|
|
860
858
|
});
|
|
861
|
-
const
|
|
859
|
+
const { depthFromFront, stackSizeAtOpen } = useModalStack(isOpen);
|
|
860
|
+
const dismiss = react$1.useDismiss(context, { ...dismissOptions, enabled: depthFromFront === 0 });
|
|
862
861
|
const { getFloatingProps } = react$1.useInteractions([dismiss]);
|
|
863
862
|
const open = react.useCallback(() => {
|
|
864
863
|
onOpenRef.current?.();
|
|
@@ -914,7 +913,22 @@ const useModal = (props) => {
|
|
|
914
913
|
getFloatingProps,
|
|
915
914
|
},
|
|
916
915
|
role,
|
|
917
|
-
|
|
916
|
+
depthFromFront,
|
|
917
|
+
stackSizeAtOpen,
|
|
918
|
+
}), [
|
|
919
|
+
isOpen,
|
|
920
|
+
toggle,
|
|
921
|
+
open,
|
|
922
|
+
close,
|
|
923
|
+
customRef,
|
|
924
|
+
refs,
|
|
925
|
+
rootElement,
|
|
926
|
+
context,
|
|
927
|
+
getFloatingProps,
|
|
928
|
+
role,
|
|
929
|
+
depthFromFront,
|
|
930
|
+
stackSizeAtOpen,
|
|
931
|
+
]);
|
|
918
932
|
};
|
|
919
933
|
|
|
920
934
|
/*
|
package/index.esm.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx, jsxs } from 'react/jsx-runtime';
|
|
|
2
2
|
import { registerTranslations } from '@trackunit/i18n-library-translation';
|
|
3
3
|
import { useMergeRefs, FloatingOverlay, FloatingFocusManager, useFloating, shift, autoUpdate, useDismiss, useInteractions } from '@floating-ui/react';
|
|
4
4
|
import { Portal, Card, Button, Heading, Text, IconButton, Icon, useWatch } from '@trackunit/react-components';
|
|
5
|
-
import { useLayoutEffect,
|
|
5
|
+
import { useLayoutEffect, useRef, forwardRef, useId, useSyncExternalStore, useMemo, useState, useContext, useEffect, useCallback } from 'react';
|
|
6
6
|
import { cvaMerge } from '@trackunit/css-class-variance-utilities';
|
|
7
7
|
import { ModalDialogContext } from '@trackunit/react-core-contexts-api';
|
|
8
8
|
|
|
@@ -264,238 +264,6 @@ const useModalFooterBorder = (rootRef, { footerClass = "border-t", enabled = tru
|
|
|
264
264
|
}, [enabled, footerClass, bodySelector, footerSelector, rootRef]);
|
|
265
265
|
};
|
|
266
266
|
|
|
267
|
-
/**
|
|
268
|
-
* Modal Stack Registry - Cross-Bundle Modal Awareness
|
|
269
|
-
*
|
|
270
|
-
* ## Architecture Overview
|
|
271
|
-
*
|
|
272
|
-
* This module provides a registry for tracking open modals across bundle boundaries
|
|
273
|
-
* (host application + Iris app iframes). It enables proper modal stacking behavior
|
|
274
|
-
* where modals can visually indicate their depth (scale, backdrop visibility, animations).
|
|
275
|
-
*
|
|
276
|
-
* ## Why postMessage Instead of Penpal?
|
|
277
|
-
*
|
|
278
|
-
* While most host-iframe communication uses Penpal (RPC-style), modal stack changes
|
|
279
|
-
* use raw postMessage for these reasons:
|
|
280
|
-
*
|
|
281
|
-
* 1. **Package Independence**: This module is part of `@trackunit/react-modal`, a standalone
|
|
282
|
-
* UI component library. It should not depend on `iris-app-runtime` to avoid circular
|
|
283
|
-
* dependencies and keep the modal component reusable.
|
|
284
|
-
*
|
|
285
|
-
* 2. **Broadcast Semantics**: When a host modal opens, ALL iframe modals need to know
|
|
286
|
-
* simultaneously (not just one specific connection). postMessage with broadcast
|
|
287
|
-
* to all iframes fits this pattern naturally.
|
|
288
|
-
*
|
|
289
|
-
* 3. **Low Latency**: Stack count changes need real-time sync for smooth animations
|
|
290
|
-
* (scale transforms, backdrop fading). Raw postMessage has less overhead than Penpal.
|
|
291
|
-
*
|
|
292
|
-
* 4. **Module Isolation**: Each bundle (host + each iframe) gets its own instance of
|
|
293
|
-
* this registry. They need cross-window synchronization, not shared state.
|
|
294
|
-
*
|
|
295
|
-
* ## Communication Flow
|
|
296
|
-
*
|
|
297
|
-
* ```
|
|
298
|
-
* ┌─────────────────────────────────────────────────────────────────────┐
|
|
299
|
-
* │ HOST APPLICATION │
|
|
300
|
-
* │ │
|
|
301
|
-
* │ modalStackRegistry ←───────────────────────────────────────────┐ │
|
|
302
|
-
* │ │ │ │
|
|
303
|
-
* │ │ subscribe() │ │
|
|
304
|
-
* │ ▼ │ │
|
|
305
|
-
* │ ModalDialogProviderHost │ │
|
|
306
|
-
* │ │ │ │
|
|
307
|
-
* │ ├─► broadcasts MODAL_STACK_MESSAGE_TYPE to all iframes │ │
|
|
308
|
-
* │ │ │ │
|
|
309
|
-
* │ └─► listens for IFRAME_MODAL_STACK_MESSAGE_TYPE ──────────┘ │
|
|
310
|
-
* │ (aggregates per-iframe, cleans up on iframe removal) │
|
|
311
|
-
* └─────────────────────────────────────────────────────────────────────┘
|
|
312
|
-
* │
|
|
313
|
-
* postMessage (bidirectional)
|
|
314
|
-
* │
|
|
315
|
-
* ┌─────────────────────────────────────────────────────────────────────┐
|
|
316
|
-
* │ IFRAME (Iris App) │
|
|
317
|
-
* │ │
|
|
318
|
-
* │ modalStackRegistry (separate instance) │
|
|
319
|
-
* │ │ │
|
|
320
|
-
* │ ├─► listens for MODAL_STACK_MESSAGE_TYPE from host │
|
|
321
|
-
* │ │ (updates hostModalCount) │
|
|
322
|
-
* │ │ │
|
|
323
|
-
* │ └─► on register/unregister, broadcasts │
|
|
324
|
-
* │ IFRAME_MODAL_STACK_MESSAGE_TYPE to parent │
|
|
325
|
-
* └─────────────────────────────────────────────────────────────────────┘
|
|
326
|
-
* ```
|
|
327
|
-
*
|
|
328
|
-
* ## Usage with useModalStack Hook
|
|
329
|
-
*
|
|
330
|
-
* The `useModalStack` hook consumes this registry to calculate:
|
|
331
|
-
* - `depthFromFront`: How many modals are in front of this one (0 = frontmost)
|
|
332
|
-
* - `stackSize`: Total modals open (local + host + iframe)
|
|
333
|
-
* - `stackSizeAtOpen`: Stack size when this modal opened (for animation decisions)
|
|
334
|
-
*
|
|
335
|
-
* This enables the Modal component to:
|
|
336
|
-
* - Scale down non-frontmost modals (visual stacking effect)
|
|
337
|
-
* - Show backdrop only on the frontmost modal
|
|
338
|
-
* - Choose appropriate animations based on whether it's opening over another modal
|
|
339
|
-
*
|
|
340
|
-
* @module modalStackRegistry
|
|
341
|
-
*/
|
|
342
|
-
let modalStack = [];
|
|
343
|
-
let hostModalCount = 0;
|
|
344
|
-
let iframeModalCount = 0;
|
|
345
|
-
const listeners = new Set();
|
|
346
|
-
const notify = () => listeners.forEach(listener => listener());
|
|
347
|
-
/**
|
|
348
|
-
* Message type for host → iframe modal stack communication.
|
|
349
|
-
* The host broadcasts this to all iframes when its modal count changes.
|
|
350
|
-
* Iframes listen for this to update their `hostModalCount`.
|
|
351
|
-
*/
|
|
352
|
-
const MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_HOST_MODAL_STACK_CHANGE";
|
|
353
|
-
/**
|
|
354
|
-
* Message type for iframe → host modal stack communication.
|
|
355
|
-
* Each iframe sends this to the parent when its modal count changes.
|
|
356
|
-
* The host aggregates these per-iframe for accurate total tracking.
|
|
357
|
-
*/
|
|
358
|
-
const IFRAME_MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_IFRAME_MODAL_STACK_CHANGE";
|
|
359
|
-
const isInIframe = typeof window !== "undefined" && window.parent !== window;
|
|
360
|
-
/**
|
|
361
|
-
* Broadcast this iframe's modal count to the parent (host).
|
|
362
|
-
* Only runs when in an iframe context. Called automatically on register/unregister.
|
|
363
|
-
*/
|
|
364
|
-
const broadcastToParent = (modalCount) => {
|
|
365
|
-
if (isInIframe) {
|
|
366
|
-
window.parent.postMessage({ type: IFRAME_MODAL_STACK_MESSAGE_TYPE, data: { iframeModalCount: modalCount } }, "*");
|
|
367
|
-
}
|
|
368
|
-
};
|
|
369
|
-
/**
|
|
370
|
-
* Set up the global message listener for cross-bundle modal stack communication.
|
|
371
|
-
* This listener runs in the same bundle context as the Modal components, ensuring
|
|
372
|
-
* the correct registry instance is updated.
|
|
373
|
-
*
|
|
374
|
-
* Note: The host also has a separate listener in ModalDialogProviderHost that
|
|
375
|
-
* tracks per-iframe counts and handles cleanup when iframes are removed.
|
|
376
|
-
*/
|
|
377
|
-
if (typeof window !== "undefined") {
|
|
378
|
-
window.addEventListener("message", event => {
|
|
379
|
-
// Host → Iframe: Update host modal count
|
|
380
|
-
if (event.data?.type === MODAL_STACK_MESSAGE_TYPE && typeof event.data?.data?.hostModalCount === "number") {
|
|
381
|
-
modalStackRegistry.setHostModalCount(event.data.data.hostModalCount);
|
|
382
|
-
}
|
|
383
|
-
// Iframe → Host: Update iframe modal count (fallback, primary handler is in ModalDialogProviderHost)
|
|
384
|
-
if (event.data?.type === IFRAME_MODAL_STACK_MESSAGE_TYPE &&
|
|
385
|
-
typeof event.data?.data?.iframeModalCount === "number") {
|
|
386
|
-
modalStackRegistry.setIframeModalCount(event.data.data.iframeModalCount);
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Module-level registry to track the stack of open modals.
|
|
392
|
-
*
|
|
393
|
-
* This registry enables:
|
|
394
|
-
* - Tracking modal order within a single bundle (local stack)
|
|
395
|
-
* - Awareness of modals in other bundles (host ↔ iframe communication)
|
|
396
|
-
* - Subscriber notifications for React integration via useSyncExternalStore
|
|
397
|
-
*
|
|
398
|
-
* Cross-bundle counts:
|
|
399
|
-
* - `hostModalCount`: Number of modals open in the host (relevant for iframes)
|
|
400
|
-
* - `iframeModalCount`: Number of modals open in iframes (relevant for host)
|
|
401
|
-
*
|
|
402
|
-
* Note: This is intentionally NOT a React hook because:
|
|
403
|
-
* 1. It needs to be a singleton that persists across React component lifecycles
|
|
404
|
-
* 2. It must work with postMessage for cross-bundle communication (host ↔ iframe)
|
|
405
|
-
* 3. The useModalStack hook consumes this registry via useSyncExternalStore for React integration
|
|
406
|
-
*/
|
|
407
|
-
const modalStackRegistry = {
|
|
408
|
-
register: (id) => {
|
|
409
|
-
modalStack.push(id);
|
|
410
|
-
notify();
|
|
411
|
-
// If in iframe, notify parent of our modal count
|
|
412
|
-
broadcastToParent(modalStack.length);
|
|
413
|
-
},
|
|
414
|
-
unregister: (id) => {
|
|
415
|
-
modalStack = modalStack.filter(modalId => modalId !== id);
|
|
416
|
-
notify();
|
|
417
|
-
// If in iframe, notify parent of our modal count
|
|
418
|
-
broadcastToParent(modalStack.length);
|
|
419
|
-
},
|
|
420
|
-
getStackPosition: (id) => modalStack.indexOf(id),
|
|
421
|
-
getStackSize: () => modalStack.length,
|
|
422
|
-
/** Get the number of modals open in the host (only relevant for Iris apps in iframes) */
|
|
423
|
-
getHostModalCount: () => hostModalCount,
|
|
424
|
-
/** Get the number of modals open in iframes (only relevant for host) */
|
|
425
|
-
getIframeModalCount: () => iframeModalCount,
|
|
426
|
-
/** Set the host modal count (called when receiving messages from host) */
|
|
427
|
-
setHostModalCount: (count) => {
|
|
428
|
-
if (hostModalCount !== count) {
|
|
429
|
-
hostModalCount = count;
|
|
430
|
-
notify();
|
|
431
|
-
}
|
|
432
|
-
},
|
|
433
|
-
/** Set the iframe modal count (called when receiving messages from iframes) */
|
|
434
|
-
setIframeModalCount: (count) => {
|
|
435
|
-
if (iframeModalCount !== count) {
|
|
436
|
-
iframeModalCount = count;
|
|
437
|
-
notify();
|
|
438
|
-
}
|
|
439
|
-
},
|
|
440
|
-
subscribe: (listener) => {
|
|
441
|
-
listeners.add(listener);
|
|
442
|
-
return () => {
|
|
443
|
-
listeners.delete(listener);
|
|
444
|
-
};
|
|
445
|
-
},
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
// Track the stack size when each modal opened (includes host modals for proper animation)
|
|
449
|
-
const modalOpenState = new Map();
|
|
450
|
-
/**
|
|
451
|
-
* Hook to track this modal's position in the stack of open modals.
|
|
452
|
-
* Returns the depth from front (0 = frontmost, 1 = one modal in front, etc.)
|
|
453
|
-
*
|
|
454
|
-
* This hook is aware of modals across bundle boundaries (host + Iris apps)
|
|
455
|
-
* by listening for host modal count changes via postMessage.
|
|
456
|
-
*
|
|
457
|
-
* @param isOpen - Whether the modal is currently open
|
|
458
|
-
*/
|
|
459
|
-
const useModalStack = (isOpen) => {
|
|
460
|
-
const modalId = useId();
|
|
461
|
-
// Subscribe to stack changes to re-render when stack updates
|
|
462
|
-
const localStackSize = useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getStackSize, modalStackRegistry.getStackSize);
|
|
463
|
-
// Subscribe to host modal count (only relevant for Iris apps in iframes)
|
|
464
|
-
const hostModalCount = useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getHostModalCount, modalStackRegistry.getHostModalCount);
|
|
465
|
-
// Subscribe to iframe modal count (only relevant for host)
|
|
466
|
-
const iframeModalCount = useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getIframeModalCount, modalStackRegistry.getIframeModalCount);
|
|
467
|
-
const stackPosition = useSyncExternalStore(modalStackRegistry.subscribe, () => modalStackRegistry.getStackPosition(modalId), () => modalStackRegistry.getStackPosition(modalId));
|
|
468
|
-
// Register/unregister based on open state
|
|
469
|
-
// useLayoutEffect ensures registration happens before paint to avoid backdrop flash
|
|
470
|
-
useLayoutEffect(() => {
|
|
471
|
-
if (isOpen) {
|
|
472
|
-
// Capture total stack size before registering (includes cross-bundle modals for proper animation)
|
|
473
|
-
// - hostModalCount: modals in host (relevant for iframes)
|
|
474
|
-
// - iframeModalCount: modals in iframes (relevant for host)
|
|
475
|
-
const totalSizeAtOpen = modalStackRegistry.getStackSize() +
|
|
476
|
-
modalStackRegistry.getHostModalCount() +
|
|
477
|
-
modalStackRegistry.getIframeModalCount();
|
|
478
|
-
modalOpenState.set(modalId, totalSizeAtOpen);
|
|
479
|
-
modalStackRegistry.register(modalId);
|
|
480
|
-
return () => {
|
|
481
|
-
modalStackRegistry.unregister(modalId);
|
|
482
|
-
modalOpenState.delete(modalId);
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
return undefined;
|
|
486
|
-
}, [isOpen, modalId]);
|
|
487
|
-
// Total stack size includes local modals, host modals, and iframe modals
|
|
488
|
-
const totalStackSize = localStackSize + hostModalCount + iframeModalCount;
|
|
489
|
-
// Calculate depth from front (0 = frontmost)
|
|
490
|
-
// Host modals are always "on top" of Iris app modals, so if hostModalCount > 0,
|
|
491
|
-
// all Iris app modals have at least that many modals in front of them
|
|
492
|
-
const localDepthFromFront = stackPosition >= 0 ? localStackSize - 1 - stackPosition : 0;
|
|
493
|
-
const depthFromFront = localDepthFromFront + hostModalCount;
|
|
494
|
-
// Get the total stack size when this modal opened (stable once set)
|
|
495
|
-
const stackSizeAtOpen = modalOpenState.get(modalId) ?? 0;
|
|
496
|
-
return useMemo(() => ({ depthFromFront, stackSize: totalStackSize, stackSizeAtOpen }), [depthFromFront, totalStackSize, stackSizeAtOpen]);
|
|
497
|
-
};
|
|
498
|
-
|
|
499
267
|
/** Scale factor per modal depth level (5% smaller per level back) */
|
|
500
268
|
const SCALE_FACTOR_PER_LEVEL = 0.05;
|
|
501
269
|
/**
|
|
@@ -533,7 +301,7 @@ const SCALE_FACTOR_PER_LEVEL = 0.05;
|
|
|
533
301
|
* @param {ModalProps} props - The props for the Modal component
|
|
534
302
|
* @returns {ReactElement} Modal component
|
|
535
303
|
*/
|
|
536
|
-
const Modal = ({ children, isOpen, role = "dialog", "data-testid": dataTestId, className, size, floatingUi, ref, restoreFocus = true, }) => {
|
|
304
|
+
const Modal = ({ children, isOpen, role = "dialog", "data-testid": dataTestId, className, size, floatingUi, ref, restoreFocus = true, depthFromFront, stackSizeAtOpen, }) => {
|
|
537
305
|
// For dialogs/modals, Floating UI recommends not using floatingStyles since the modal
|
|
538
306
|
// is viewport-centered via CSS, not positioned relative to a reference element.
|
|
539
307
|
// See: https://floating-ui.com/docs/dialog
|
|
@@ -542,8 +310,6 @@ const Modal = ({ children, isOpen, role = "dialog", "data-testid": dataTestId, c
|
|
|
542
310
|
// Merge the custom ref from useModal with FloatingUI's setFloating ref
|
|
543
311
|
const mergedRef = useMergeRefs([refs.setFloating, ref]);
|
|
544
312
|
useModalFooterBorder(cardRef, { enabled: isOpen, footerClass: "border-t pt-4" });
|
|
545
|
-
// Track modal stack position for stacked modal styling
|
|
546
|
-
const { depthFromFront, stackSizeAtOpen } = useModalStack(isOpen);
|
|
547
313
|
const isFrontmost = depthFromFront === 0;
|
|
548
314
|
return (jsx(Portal, { root: rootElement, children: isOpen ? (jsx(FloatingOverlay, { className: cvaModalBackdrop({ isFrontmost, shouldAnimate: stackSizeAtOpen === 0 }), lockScroll: true, children: jsx(FloatingFocusManager, { context: context, restoreFocus: restoreFocus, children: jsx("div", { "aria-modal": true, className: cvaModalContainer(), ref: mergedRef, role: role,
|
|
549
315
|
// Scale down modals that are behind the frontmost
|
|
@@ -761,6 +527,238 @@ const ModalHeader = forwardRef(({ heading, subHeading, onClickClose, "data-testi
|
|
|
761
527
|
}), "data-testid": dataTestId, id: id, ref: ref, children: [jsxs("div", { className: cvaHeadingContainer(), children: [jsxs("div", { className: cvaTitleContainer(), children: [jsx(Heading, { variant: "tertiary", children: heading }), accessories] }), Boolean(subHeading) ? (jsx(Text, { size: "small", subtle: true, children: subHeading })) : null, children] }), jsx("div", { className: cvaIconContainer(), children: jsx(IconButton, { className: "!h-min", "data-testid": dataTestId ? `${dataTestId}-close-button` : "modal-close-button", icon: jsx(Icon, { name: "XMark", size: "small" }), onClick: onClickClose, size: "small", variant: "ghost-neutral" }) })] }));
|
|
762
528
|
});
|
|
763
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Modal Stack Registry - Cross-Bundle Modal Awareness
|
|
532
|
+
*
|
|
533
|
+
* ## Architecture Overview
|
|
534
|
+
*
|
|
535
|
+
* This module provides a registry for tracking open modals across bundle boundaries
|
|
536
|
+
* (host application + Iris app iframes). It enables proper modal stacking behavior
|
|
537
|
+
* where modals can visually indicate their depth (scale, backdrop visibility, animations).
|
|
538
|
+
*
|
|
539
|
+
* ## Why postMessage Instead of Penpal?
|
|
540
|
+
*
|
|
541
|
+
* While most host-iframe communication uses Penpal (RPC-style), modal stack changes
|
|
542
|
+
* use raw postMessage for these reasons:
|
|
543
|
+
*
|
|
544
|
+
* 1. **Package Independence**: This module is part of `@trackunit/react-modal`, a standalone
|
|
545
|
+
* UI component library. It should not depend on `iris-app-runtime` to avoid circular
|
|
546
|
+
* dependencies and keep the modal component reusable.
|
|
547
|
+
*
|
|
548
|
+
* 2. **Broadcast Semantics**: When a host modal opens, ALL iframe modals need to know
|
|
549
|
+
* simultaneously (not just one specific connection). postMessage with broadcast
|
|
550
|
+
* to all iframes fits this pattern naturally.
|
|
551
|
+
*
|
|
552
|
+
* 3. **Low Latency**: Stack count changes need real-time sync for smooth animations
|
|
553
|
+
* (scale transforms, backdrop fading). Raw postMessage has less overhead than Penpal.
|
|
554
|
+
*
|
|
555
|
+
* 4. **Module Isolation**: Each bundle (host + each iframe) gets its own instance of
|
|
556
|
+
* this registry. They need cross-window synchronization, not shared state.
|
|
557
|
+
*
|
|
558
|
+
* ## Communication Flow
|
|
559
|
+
*
|
|
560
|
+
* ```
|
|
561
|
+
* ┌─────────────────────────────────────────────────────────────────────┐
|
|
562
|
+
* │ HOST APPLICATION │
|
|
563
|
+
* │ │
|
|
564
|
+
* │ modalStackRegistry ←───────────────────────────────────────────┐ │
|
|
565
|
+
* │ │ │ │
|
|
566
|
+
* │ │ subscribe() │ │
|
|
567
|
+
* │ ▼ │ │
|
|
568
|
+
* │ ModalDialogProviderHost │ │
|
|
569
|
+
* │ │ │ │
|
|
570
|
+
* │ ├─► broadcasts MODAL_STACK_MESSAGE_TYPE to all iframes │ │
|
|
571
|
+
* │ │ │ │
|
|
572
|
+
* │ └─► listens for IFRAME_MODAL_STACK_MESSAGE_TYPE ──────────┘ │
|
|
573
|
+
* │ (aggregates per-iframe, cleans up on iframe removal) │
|
|
574
|
+
* └─────────────────────────────────────────────────────────────────────┘
|
|
575
|
+
* │
|
|
576
|
+
* postMessage (bidirectional)
|
|
577
|
+
* │
|
|
578
|
+
* ┌─────────────────────────────────────────────────────────────────────┐
|
|
579
|
+
* │ IFRAME (Iris App) │
|
|
580
|
+
* │ │
|
|
581
|
+
* │ modalStackRegistry (separate instance) │
|
|
582
|
+
* │ │ │
|
|
583
|
+
* │ ├─► listens for MODAL_STACK_MESSAGE_TYPE from host │
|
|
584
|
+
* │ │ (updates hostModalCount) │
|
|
585
|
+
* │ │ │
|
|
586
|
+
* │ └─► on register/unregister, broadcasts │
|
|
587
|
+
* │ IFRAME_MODAL_STACK_MESSAGE_TYPE to parent │
|
|
588
|
+
* └─────────────────────────────────────────────────────────────────────┘
|
|
589
|
+
* ```
|
|
590
|
+
*
|
|
591
|
+
* ## Usage with useModalStack Hook
|
|
592
|
+
*
|
|
593
|
+
* The `useModalStack` hook consumes this registry to calculate:
|
|
594
|
+
* - `depthFromFront`: How many modals are in front of this one (0 = frontmost)
|
|
595
|
+
* - `stackSize`: Total modals open (local + host + iframe)
|
|
596
|
+
* - `stackSizeAtOpen`: Stack size when this modal opened (for animation decisions)
|
|
597
|
+
*
|
|
598
|
+
* This enables the Modal component to:
|
|
599
|
+
* - Scale down non-frontmost modals (visual stacking effect)
|
|
600
|
+
* - Show backdrop only on the frontmost modal
|
|
601
|
+
* - Choose appropriate animations based on whether it's opening over another modal
|
|
602
|
+
*
|
|
603
|
+
* @module modalStackRegistry
|
|
604
|
+
*/
|
|
605
|
+
let modalStack = [];
|
|
606
|
+
let hostModalCount = 0;
|
|
607
|
+
let iframeModalCount = 0;
|
|
608
|
+
const listeners = new Set();
|
|
609
|
+
const notify = () => listeners.forEach(listener => listener());
|
|
610
|
+
/**
|
|
611
|
+
* Message type for host → iframe modal stack communication.
|
|
612
|
+
* The host broadcasts this to all iframes when its modal count changes.
|
|
613
|
+
* Iframes listen for this to update their `hostModalCount`.
|
|
614
|
+
*/
|
|
615
|
+
const MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_HOST_MODAL_STACK_CHANGE";
|
|
616
|
+
/**
|
|
617
|
+
* Message type for iframe → host modal stack communication.
|
|
618
|
+
* Each iframe sends this to the parent when its modal count changes.
|
|
619
|
+
* The host aggregates these per-iframe for accurate total tracking.
|
|
620
|
+
*/
|
|
621
|
+
const IFRAME_MODAL_STACK_MESSAGE_TYPE = "IRIS_APP_IFRAME_MODAL_STACK_CHANGE";
|
|
622
|
+
const isInIframe = typeof window !== "undefined" && window.parent !== window;
|
|
623
|
+
/**
|
|
624
|
+
* Broadcast this iframe's modal count to the parent (host).
|
|
625
|
+
* Only runs when in an iframe context. Called automatically on register/unregister.
|
|
626
|
+
*/
|
|
627
|
+
const broadcastToParent = (modalCount) => {
|
|
628
|
+
if (isInIframe) {
|
|
629
|
+
window.parent.postMessage({ type: IFRAME_MODAL_STACK_MESSAGE_TYPE, data: { iframeModalCount: modalCount } }, "*");
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
/**
|
|
633
|
+
* Set up the global message listener for cross-bundle modal stack communication.
|
|
634
|
+
* This listener runs in the same bundle context as the Modal components, ensuring
|
|
635
|
+
* the correct registry instance is updated.
|
|
636
|
+
*
|
|
637
|
+
* Note: The host also has a separate listener in ModalDialogProviderHost that
|
|
638
|
+
* tracks per-iframe counts and handles cleanup when iframes are removed.
|
|
639
|
+
*/
|
|
640
|
+
if (typeof window !== "undefined") {
|
|
641
|
+
window.addEventListener("message", event => {
|
|
642
|
+
// Host → Iframe: Update host modal count
|
|
643
|
+
if (event.data?.type === MODAL_STACK_MESSAGE_TYPE && typeof event.data?.data?.hostModalCount === "number") {
|
|
644
|
+
modalStackRegistry.setHostModalCount(event.data.data.hostModalCount);
|
|
645
|
+
}
|
|
646
|
+
// Iframe → Host: Update iframe modal count (fallback, primary handler is in ModalDialogProviderHost)
|
|
647
|
+
if (event.data?.type === IFRAME_MODAL_STACK_MESSAGE_TYPE &&
|
|
648
|
+
typeof event.data?.data?.iframeModalCount === "number") {
|
|
649
|
+
modalStackRegistry.setIframeModalCount(event.data.data.iframeModalCount);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Module-level registry to track the stack of open modals.
|
|
655
|
+
*
|
|
656
|
+
* This registry enables:
|
|
657
|
+
* - Tracking modal order within a single bundle (local stack)
|
|
658
|
+
* - Awareness of modals in other bundles (host ↔ iframe communication)
|
|
659
|
+
* - Subscriber notifications for React integration via useSyncExternalStore
|
|
660
|
+
*
|
|
661
|
+
* Cross-bundle counts:
|
|
662
|
+
* - `hostModalCount`: Number of modals open in the host (relevant for iframes)
|
|
663
|
+
* - `iframeModalCount`: Number of modals open in iframes (relevant for host)
|
|
664
|
+
*
|
|
665
|
+
* Note: This is intentionally NOT a React hook because:
|
|
666
|
+
* 1. It needs to be a singleton that persists across React component lifecycles
|
|
667
|
+
* 2. It must work with postMessage for cross-bundle communication (host ↔ iframe)
|
|
668
|
+
* 3. The useModalStack hook consumes this registry via useSyncExternalStore for React integration
|
|
669
|
+
*/
|
|
670
|
+
const modalStackRegistry = {
|
|
671
|
+
register: (id) => {
|
|
672
|
+
modalStack.push(id);
|
|
673
|
+
notify();
|
|
674
|
+
// If in iframe, notify parent of our modal count
|
|
675
|
+
broadcastToParent(modalStack.length);
|
|
676
|
+
},
|
|
677
|
+
unregister: (id) => {
|
|
678
|
+
modalStack = modalStack.filter(modalId => modalId !== id);
|
|
679
|
+
notify();
|
|
680
|
+
// If in iframe, notify parent of our modal count
|
|
681
|
+
broadcastToParent(modalStack.length);
|
|
682
|
+
},
|
|
683
|
+
getStackPosition: (id) => modalStack.indexOf(id),
|
|
684
|
+
getStackSize: () => modalStack.length,
|
|
685
|
+
/** Get the number of modals open in the host (only relevant for Iris apps in iframes) */
|
|
686
|
+
getHostModalCount: () => hostModalCount,
|
|
687
|
+
/** Get the number of modals open in iframes (only relevant for host) */
|
|
688
|
+
getIframeModalCount: () => iframeModalCount,
|
|
689
|
+
/** Set the host modal count (called when receiving messages from host) */
|
|
690
|
+
setHostModalCount: (count) => {
|
|
691
|
+
if (hostModalCount !== count) {
|
|
692
|
+
hostModalCount = count;
|
|
693
|
+
notify();
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
/** Set the iframe modal count (called when receiving messages from iframes) */
|
|
697
|
+
setIframeModalCount: (count) => {
|
|
698
|
+
if (iframeModalCount !== count) {
|
|
699
|
+
iframeModalCount = count;
|
|
700
|
+
notify();
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
subscribe: (listener) => {
|
|
704
|
+
listeners.add(listener);
|
|
705
|
+
return () => {
|
|
706
|
+
listeners.delete(listener);
|
|
707
|
+
};
|
|
708
|
+
},
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// Track the stack size when each modal opened (includes host modals for proper animation)
|
|
712
|
+
const modalOpenState = new Map();
|
|
713
|
+
/**
|
|
714
|
+
* Hook to track this modal's position in the stack of open modals.
|
|
715
|
+
* Returns the depth from front (0 = frontmost, 1 = one modal in front, etc.)
|
|
716
|
+
*
|
|
717
|
+
* This hook is aware of modals across bundle boundaries (host + Iris apps)
|
|
718
|
+
* by listening for host modal count changes via postMessage.
|
|
719
|
+
*
|
|
720
|
+
* @param isOpen - Whether the modal is currently open
|
|
721
|
+
*/
|
|
722
|
+
const useModalStack = (isOpen) => {
|
|
723
|
+
const modalId = useId();
|
|
724
|
+
// Subscribe to stack changes to re-render when stack updates
|
|
725
|
+
const localStackSize = useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getStackSize, modalStackRegistry.getStackSize);
|
|
726
|
+
// Subscribe to host modal count (only relevant for Iris apps in iframes)
|
|
727
|
+
const hostModalCount = useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getHostModalCount, modalStackRegistry.getHostModalCount);
|
|
728
|
+
// Subscribe to iframe modal count (only relevant for host)
|
|
729
|
+
const iframeModalCount = useSyncExternalStore(modalStackRegistry.subscribe, modalStackRegistry.getIframeModalCount, modalStackRegistry.getIframeModalCount);
|
|
730
|
+
const stackPosition = useSyncExternalStore(modalStackRegistry.subscribe, () => modalStackRegistry.getStackPosition(modalId), () => modalStackRegistry.getStackPosition(modalId));
|
|
731
|
+
// Register/unregister based on open state
|
|
732
|
+
// useLayoutEffect ensures registration happens before paint to avoid backdrop flash
|
|
733
|
+
useLayoutEffect(() => {
|
|
734
|
+
if (isOpen) {
|
|
735
|
+
// Capture total stack size before registering (includes cross-bundle modals for proper animation)
|
|
736
|
+
// - hostModalCount: modals in host (relevant for iframes)
|
|
737
|
+
// - iframeModalCount: modals in iframes (relevant for host)
|
|
738
|
+
const totalSizeAtOpen = modalStackRegistry.getStackSize() +
|
|
739
|
+
modalStackRegistry.getHostModalCount() +
|
|
740
|
+
modalStackRegistry.getIframeModalCount();
|
|
741
|
+
modalOpenState.set(modalId, totalSizeAtOpen);
|
|
742
|
+
modalStackRegistry.register(modalId);
|
|
743
|
+
return () => {
|
|
744
|
+
modalStackRegistry.unregister(modalId);
|
|
745
|
+
modalOpenState.delete(modalId);
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return undefined;
|
|
749
|
+
}, [isOpen, modalId]);
|
|
750
|
+
// Total stack size includes local modals, host modals, and iframe modals
|
|
751
|
+
const totalStackSize = localStackSize + hostModalCount + iframeModalCount;
|
|
752
|
+
// Calculate depth from front (0 = frontmost)
|
|
753
|
+
// Host modals are always "on top" of Iris app modals, so if hostModalCount > 0,
|
|
754
|
+
// all Iris app modals have at least that many modals in front of them
|
|
755
|
+
const localDepthFromFront = stackPosition >= 0 ? localStackSize - 1 - stackPosition : 0;
|
|
756
|
+
const depthFromFront = localDepthFromFront + hostModalCount;
|
|
757
|
+
// Get the total stack size when this modal opened (stable once set)
|
|
758
|
+
const stackSizeAtOpen = modalOpenState.get(modalId) ?? 0;
|
|
759
|
+
return useMemo(() => ({ depthFromFront, stackSize: totalStackSize, stackSizeAtOpen }), [depthFromFront, totalStackSize, stackSizeAtOpen]);
|
|
760
|
+
};
|
|
761
|
+
|
|
764
762
|
/**
|
|
765
763
|
* A hook to handle the state and configuration of Modal components.
|
|
766
764
|
*
|
|
@@ -856,7 +854,8 @@ const useModal = (props) => {
|
|
|
856
854
|
whileElementsMounted: autoUpdate,
|
|
857
855
|
middleware: [shift()],
|
|
858
856
|
});
|
|
859
|
-
const
|
|
857
|
+
const { depthFromFront, stackSizeAtOpen } = useModalStack(isOpen);
|
|
858
|
+
const dismiss = useDismiss(context, { ...dismissOptions, enabled: depthFromFront === 0 });
|
|
860
859
|
const { getFloatingProps } = useInteractions([dismiss]);
|
|
861
860
|
const open = useCallback(() => {
|
|
862
861
|
onOpenRef.current?.();
|
|
@@ -912,7 +911,22 @@ const useModal = (props) => {
|
|
|
912
911
|
getFloatingProps,
|
|
913
912
|
},
|
|
914
913
|
role,
|
|
915
|
-
|
|
914
|
+
depthFromFront,
|
|
915
|
+
stackSizeAtOpen,
|
|
916
|
+
}), [
|
|
917
|
+
isOpen,
|
|
918
|
+
toggle,
|
|
919
|
+
open,
|
|
920
|
+
close,
|
|
921
|
+
customRef,
|
|
922
|
+
refs,
|
|
923
|
+
rootElement,
|
|
924
|
+
context,
|
|
925
|
+
getFloatingProps,
|
|
926
|
+
role,
|
|
927
|
+
depthFromFront,
|
|
928
|
+
stackSizeAtOpen,
|
|
929
|
+
]);
|
|
916
930
|
};
|
|
917
931
|
|
|
918
932
|
/*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-modal",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.11",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@floating-ui/react": "^0.26.25",
|
|
11
|
-
"@trackunit/react-components": "1.21.
|
|
11
|
+
"@trackunit/react-components": "1.21.8",
|
|
12
12
|
"@trackunit/css-class-variance-utilities": "1.11.92",
|
|
13
13
|
"@trackunit/shared-utils": "1.13.92",
|
|
14
14
|
"@floating-ui/react-dom": "2.1.2",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"@trackunit/i18n-library-translation": "1.17.6"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
|
+
"@tanstack/react-router": "^1.114.29",
|
|
19
20
|
"react": "^19.0.0"
|
|
20
21
|
},
|
|
21
22
|
"module": "./index.esm.js",
|
package/src/modal/Modal.d.ts
CHANGED
|
@@ -61,6 +61,6 @@ export type ModalProps = PropsWithChildren<UseModalReturnValue & {
|
|
|
61
61
|
* @returns {ReactElement} Modal component
|
|
62
62
|
*/
|
|
63
63
|
export declare const Modal: {
|
|
64
|
-
({ children, isOpen, role, "data-testid": dataTestId, className, size, floatingUi, ref, restoreFocus, }: ModalProps): ReactElement;
|
|
64
|
+
({ children, isOpen, role, "data-testid": dataTestId, className, size, floatingUi, ref, restoreFocus, depthFromFront, stackSizeAtOpen, }: ModalProps): ReactElement;
|
|
65
65
|
displayName: string;
|
|
66
66
|
};
|
package/src/modal/useModal.d.ts
CHANGED
|
@@ -133,6 +133,16 @@ export type UseModalReturnValue = {
|
|
|
133
133
|
* The aria role for the modal.
|
|
134
134
|
*/
|
|
135
135
|
role?: AriaRole;
|
|
136
|
+
/**
|
|
137
|
+
* How many modals are stacked in front of this one (0 = frontmost).
|
|
138
|
+
* Used by Modal for visual stacking (scale, backdrop).
|
|
139
|
+
*/
|
|
140
|
+
depthFromFront: number;
|
|
141
|
+
/**
|
|
142
|
+
* The total modal stack size when this modal opened.
|
|
143
|
+
* Used by Modal for animation decisions.
|
|
144
|
+
*/
|
|
145
|
+
stackSizeAtOpen: number;
|
|
136
146
|
};
|
|
137
147
|
/**
|
|
138
148
|
* A hook to handle the state and configuration of Modal components.
|