@cdevhub/ngx-tw 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, signal, Injector, Injectable, makeEnvironmentProviders, Directive, input } from '@angular/core';
3
- import { DialogConfig, CdkDialogContainer, Dialog } from '@angular/cdk/dialog';
4
- import { createBlockScrollStrategy, createNoopScrollStrategy, createRepositionScrollStrategy, createCloseScrollStrategy } from '@angular/cdk/overlay';
5
- import { ReplaySubject, filter, take, merge, Subject, defer, startWith } from 'rxjs';
6
- import { CdkPortalOutlet } from '@angular/cdk/portal';
7
- import { OverlayContainerCoordinator, mergeOverlayPanelClass, coerceOverlayDuration } from '@cdevhub/ngx-tw/core';
8
- import { tv } from 'tailwind-variants';
2
+ import { InjectionToken, signal, inject, Injector, Injectable, makeEnvironmentProviders, Directive, input, computed } from '@angular/core';
3
+ import { ReplaySubject, Subject, filter, take, merge, defer, startWith } from 'rxjs';
4
+ import { DialogConfig } from '@angular/cdk/dialog';
9
5
  import { ESCAPE, hasModifierKey } from '@angular/cdk/keycodes';
10
6
  import { _IdGenerator } from '@angular/cdk/a11y';
11
7
  import * as i1 from '@angular/cdk/scrolling';
@@ -55,130 +51,22 @@ const TW_DIALOG_DATA = new InjectionToken('TW_DIALOG_DATA');
55
51
  /** Injection token for application-wide default dialog options. Set via `provideTwDialog()`. */
56
52
  const TW_DIALOG_DEFAULT_OPTIONS = new InjectionToken('TW_DIALOG_DEFAULT_OPTIONS');
57
53
 
58
- // Dialog uses a `data-[state]` driven CSS transition rather than the project's
59
- // `animate.enter`/`animate.leave` keyframes. Justification:
60
- // 1. The ref owns the open/close lifecycle (state signal, observables) and
61
- // needs runtime-configurable enter/exit durations per `TwDialog.open()`
62
- // call — `animate.enter` only accepts a static class name.
63
- // 2. The container drives the same CSS variables (opacity + scale) for both
64
- // transitions, so a single `transition-[opacity,transform]` rule with a
65
- // data-state attribute is simpler than two keyframe declarations.
66
- // All other library overlays (popover, menu, tooltip) use animate.enter/leave;
67
- // this divergence is intentional and isolated to the dialog container.
68
- const dialogContainerVariants = tv({
69
- slots: {
70
- host: 'relative flex flex-col outline-none bg-surface-raised text-fg rounded-lg shadow-md border border-border overflow-hidden transition-[opacity,transform] ease-out motion-reduce:transition-none opacity-0 scale-95 data-[state=open]:opacity-100 data-[state=open]:scale-100 data-[state=closing]:opacity-0 data-[state=closing]:scale-95',
71
- },
72
- variants: {
73
- size: {
74
- xs: { host: 'w-full max-w-sm max-h-[85vh]' },
75
- sm: { host: 'w-full max-w-md max-h-[85vh]' },
76
- md: { host: 'w-full max-w-lg max-h-[85vh]' },
77
- lg: { host: 'w-full max-w-2xl max-h-[85vh]' },
78
- xl: { host: 'w-full max-w-4xl max-h-[85vh]' },
79
- fullscreen: {
80
- host: 'w-screen h-screen max-w-none max-h-none rounded-none border-0',
81
- },
82
- },
83
- },
84
- defaultVariants: { size: 'md' },
85
- }, { twMerge: true });
86
- /**
87
- * Dialog container rendered inside the CDK overlay. Wraps the user-provided
88
- * content in a Tailwind-styled surface and coordinates enter/exit transitions
89
- * with {@link TwDialogRef}.
90
- *
91
- * The animation state machine, `aria-describedby` queue, and panel-class
92
- * merge are shared with `SheetContainer` via {@link OverlayContainerCoordinator}
93
- * (component-scoped — see `providers: [OverlayContainerCoordinator]`).
94
- *
95
- * @docs-private
96
- */
97
- class DialogContainer extends CdkDialogContainer {
98
- coordinator = inject(OverlayContainerCoordinator);
99
- /** Lifecycle state of the dialog's enter/exit animation. */
100
- state = this.coordinator.state;
101
- /** Emits whenever the animation state transitions. */
102
- animationStateChanged = this.coordinator.animationStateChanged;
103
- /** Resolved enter-animation duration in ms. */
104
- enterAnimationDuration;
105
- /** Resolved exit-animation duration in ms. */
106
- exitAnimationDuration;
107
- sizeVariant = computed(() => {
108
- const size = this._config.size ?? 'md';
109
- return dialogContainerVariants({ size });
110
- }, /* @ts-ignore */
111
- ...(ngDevMode ? [{ debugName: "sizeVariant" }] : /* istanbul ignore next */ []));
112
- hostClasses = computed(() => mergeOverlayPanelClass(this.sizeVariant().host(), this._config.panelClass), /* @ts-ignore */
113
- ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
114
- ariaDescribedByAttr = computed(() => this._config.ariaDescribedBy ||
115
- this.coordinator.describedByIds()[0] ||
116
- null, /* @ts-ignore */
117
- ...(ngDevMode ? [{ debugName: "ariaDescribedByAttr" }] : /* istanbul ignore next */ []));
118
- transitionDuration = this.coordinator.transitionDuration;
119
- constructor() {
120
- super();
121
- this.enterAnimationDuration = coerceOverlayDuration(this._config.enterAnimationDuration, 150);
122
- this.exitAnimationDuration = coerceOverlayDuration(this._config.exitAnimationDuration, 120);
123
- this.coordinator.setDurations(this.enterAnimationDuration, this.exitAnimationDuration);
124
- }
125
- _contentAttached() {
126
- super._contentAttached();
127
- this.coordinator.startEnterAnimation();
128
- }
129
- /** Triggered by the dialog ref to play the exit transition before disposing the overlay. */
130
- _startExitAnimation() {
131
- this.coordinator.startExitAnimation();
132
- }
133
- /** Registers a description id for the container's `aria-describedby`. */
134
- _addAriaDescribedBy(id) {
135
- this.coordinator.addAriaDescribedBy(id);
136
- }
137
- /** Removes a previously registered description id. */
138
- _removeAriaDescribedBy(id) {
139
- this.coordinator.removeAriaDescribedBy(id);
140
- }
141
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: DialogContainer, deps: [], target: i0.ɵɵFactoryTarget.Component });
142
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.7", type: DialogContainer, isStandalone: true, selector: "tw-dialog-container", host: { attributes: { "tabindex": "-1" }, properties: { "attr.id": "_config.id || null", "attr.role": "_config.role", "attr.aria-modal": "_config.ariaModal", "attr.aria-labelledby": "_config.ariaLabel ? null : _ariaLabelledByQueue[0]", "attr.aria-label": "_config.ariaLabel", "attr.aria-describedby": "ariaDescribedByAttr()", "attr.data-state": "state()", "class": "hostClasses()", "style.transition-duration.ms": "transitionDuration()" } }, providers: [OverlayContainerCoordinator], usesInheritance: true, ngImport: i0, template: '<ng-template cdkPortalOutlet />', isInline: true, dependencies: [{ kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
143
- }
144
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: DialogContainer, decorators: [{
145
- type: Component,
146
- args: [{
147
- selector: 'tw-dialog-container',
148
- template: '<ng-template cdkPortalOutlet />',
149
- changeDetection: ChangeDetectionStrategy.OnPush,
150
- encapsulation: ViewEncapsulation.None,
151
- imports: [CdkPortalOutlet],
152
- providers: [OverlayContainerCoordinator],
153
- host: {
154
- tabindex: '-1',
155
- '[attr.id]': '_config.id || null',
156
- '[attr.role]': '_config.role',
157
- '[attr.aria-modal]': '_config.ariaModal',
158
- '[attr.aria-labelledby]': '_config.ariaLabel ? null : _ariaLabelledByQueue[0]',
159
- '[attr.aria-label]': '_config.ariaLabel',
160
- '[attr.aria-describedby]': 'ariaDescribedByAttr()',
161
- '[attr.data-state]': 'state()',
162
- '[class]': 'hostClasses()',
163
- '[style.transition-duration.ms]': 'transitionDuration()',
164
- },
165
- }]
166
- }], ctorParameters: () => [] });
167
-
168
54
  /**
169
55
  * Reference to a dialog opened via {@link TwDialog.open}. Drives the dialog
170
56
  * lifecycle (close, state, observables) and forwards useful overlay streams.
57
+ *
58
+ * The ref is returned **synchronously** from `open()`, but the dialog's render
59
+ * layer (`@angular/cdk/dialog` + the Tailwind container) is loaded through a
60
+ * dynamic `import()`. The ref therefore starts *detached*: `id`, `state`,
61
+ * `close()`, the lifecycle observables, and panel/size mutations all work
62
+ * immediately (mutations are buffered and replayed on attach), but the rendered
63
+ * component instance does not exist yet — read it via {@link whenComponentReady}
64
+ * instead of a synchronous `componentInstance` field.
171
65
  */
172
66
  class TwDialogRef {
173
- cdkRef;
174
67
  config;
175
- containerInstance;
176
- /** Unique ID of the dialog. */
68
+ /** Unique ID of the dialog. Generated eagerly by the service, so it is valid the instant `open()` returns. */
177
69
  id;
178
- /** Instance of the component rendered inside the dialog, or `null` for template dialogs. */
179
- componentInstance = null;
180
- /** `ComponentRef` of the content component, or `null` for template dialogs. */
181
- componentRef = null;
182
70
  /** Current lifecycle state. Reactively readable. */
183
71
  state;
184
72
  /** When `true`, close-via-escape and close-via-backdrop are disabled. */
@@ -188,17 +76,58 @@ class TwDialogRef {
188
76
  afterOpenedSubject = new ReplaySubject(1);
189
77
  beforeClosedSubject = new ReplaySubject(1);
190
78
  afterClosedSubject = new ReplaySubject(1);
79
+ // Facade-owned pass-throughs for the raw overlay streams, so a consumer that
80
+ // subscribes before the render chunk attaches still receives events once it
81
+ // does — the pre-deferral behaviour when `open()` wrapped a live `cdkRef`.
82
+ backdropClickSubject = new Subject();
83
+ keydownEventsSubject = new Subject();
84
+ cdkRef = null;
85
+ container = null;
86
+ componentInstanceValue = null;
87
+ componentRefValue = null;
88
+ resolveComponentReady;
89
+ componentReadyPromise = new Promise((resolve) => {
90
+ this.resolveComponentReady = resolve;
91
+ });
92
+ // Buffered mutations issued before the render chunk attached the CDK backend.
93
+ pendingPanelAdds = [];
94
+ pendingPanelRemoves = [];
95
+ pendingSize = null;
191
96
  pendingResult;
192
97
  closeFocusOrigin;
193
- constructor(cdkRef, config, containerInstance) {
194
- this.cdkRef = cdkRef;
98
+ constructor(id, config) {
195
99
  this.config = config;
196
- this.containerInstance = containerInstance;
197
- this.id = cdkRef.id;
100
+ this.id = id;
198
101
  this.disableClose = config.disableClose;
199
102
  this.state = this.stateSignal.asReadonly();
103
+ }
104
+ /** The Tailwind container instance, or `null` until the dialog has attached. */
105
+ get containerInstance() {
106
+ return this.container;
107
+ }
108
+ /**
109
+ * @internal Wire this facade to its CDK backend. Called from the dialog
110
+ * renderer **inside** `cdkDialog.open()`'s `providers` callback — the same
111
+ * point the constructor ran before deferral — so subscriptions to
112
+ * `animationStateChanged` are in place before the container emits `'open'`.
113
+ */
114
+ _attach(cdkRef, container) {
115
+ this.cdkRef = cdkRef;
116
+ this.container = container;
117
+ // Forward the raw overlay streams into the facade pass-throughs.
118
+ cdkRef.backdropClick.subscribe(this.backdropClickSubject);
119
+ cdkRef.keydownEvents.subscribe(this.keydownEventsSubject);
200
120
  cdkRef.addPanelClass('tw-dialog-panel');
201
- const animationChanges = containerInstance.animationStateChanged;
121
+ for (const cls of this.pendingPanelAdds)
122
+ cdkRef.addPanelClass(cls);
123
+ for (const cls of this.pendingPanelRemoves)
124
+ cdkRef.removePanelClass(cls);
125
+ if (this.pendingSize)
126
+ cdkRef.updateSize(this.pendingSize[0], this.pendingSize[1]);
127
+ this.pendingPanelAdds.length = 0;
128
+ this.pendingPanelRemoves.length = 0;
129
+ this.pendingSize = null;
130
+ const animationChanges = container.animationStateChanged;
202
131
  animationChanges
203
132
  .pipe(filter((event) => event.state === 'open'), take(1))
204
133
  .subscribe(() => {
@@ -226,6 +155,38 @@ class TwDialogRef {
226
155
  this.closeWithOrigin(event.type === 'keydown' ? 'keyboard' : 'mouse');
227
156
  });
228
157
  }
158
+ /** @internal Record the rendered component after `cdkDialog.open()` returns. */
159
+ _setComponent(instance, ref) {
160
+ this.componentInstanceValue = instance;
161
+ this.componentRefValue = ref;
162
+ this.resolveComponentReady(instance);
163
+ }
164
+ /**
165
+ * Resolves with the rendered content-component instance once the dialog's
166
+ * render chunk has loaded and attached. Resolves `null` for template dialogs,
167
+ * and for a dialog closed before it ever opened.
168
+ *
169
+ * Replaces the former synchronous `componentInstance` field, which cannot be
170
+ * populated before the deferred render chunk lands.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * const ref = dialog.open(EditorDialog);
175
+ * const editor = await ref.whenComponentReady();
176
+ * editor?.focusFirstField();
177
+ * ```
178
+ */
179
+ whenComponentReady() {
180
+ return this.componentReadyPromise;
181
+ }
182
+ /** The rendered component instance, or `null` if not yet attached / a template dialog. Prefer {@link whenComponentReady}. */
183
+ get componentInstance() {
184
+ return this.componentInstanceValue;
185
+ }
186
+ /** The rendered `ComponentRef`, or `null` if not yet attached / a template dialog. */
187
+ get componentRef() {
188
+ return this.componentRefValue;
189
+ }
229
190
  /**
230
191
  * Closes the dialog. The exit animation runs before the overlay is disposed.
231
192
  * @param result Value forwarded to `afterClosed()` subscribers.
@@ -245,27 +206,42 @@ class TwDialogRef {
245
206
  afterClosed() {
246
207
  return this.afterClosedSubject.asObservable();
247
208
  }
248
- /** Backdrop click stream (emits even when `disableClose` is set). */
209
+ /** Backdrop click stream (emits even when `disableClose` is set). Buffered — a subscription made before the dialog attaches receives events once it does. */
249
210
  backdropClick() {
250
- return this.cdkRef.backdropClick;
211
+ return this.backdropClickSubject.asObservable();
251
212
  }
252
- /** Keydown event stream for the overlay. */
213
+ /** Keydown event stream for the overlay. Buffered — a subscription made before the dialog attaches receives events once it does. */
253
214
  keydownEvents() {
254
- return this.cdkRef.keydownEvents;
215
+ return this.keydownEventsSubject.asObservable();
255
216
  }
256
- /** Updates the dialog's width/height. Pass empty string to reset a dimension. */
217
+ /** Updates the dialog's width/height. Pass empty string to reset a dimension. Buffered until attach. */
257
218
  updateSize(width = '', height = '') {
258
- this.cdkRef.updateSize(width, height);
219
+ if (this.cdkRef) {
220
+ this.cdkRef.updateSize(width, height);
221
+ }
222
+ else {
223
+ this.pendingSize = [width, height];
224
+ }
259
225
  return this;
260
226
  }
261
- /** Adds CSS classes to the overlay panel. */
227
+ /** Adds CSS classes to the overlay panel. Buffered until attach. */
262
228
  addPanelClass(classes) {
263
- this.cdkRef.addPanelClass(classes);
229
+ if (this.cdkRef) {
230
+ this.cdkRef.addPanelClass(classes);
231
+ }
232
+ else {
233
+ this.pendingPanelAdds.push(...(Array.isArray(classes) ? classes : [classes]));
234
+ }
264
235
  return this;
265
236
  }
266
- /** Removes CSS classes from the overlay panel. */
237
+ /** Removes CSS classes from the overlay panel. Buffered until attach. */
267
238
  removePanelClass(classes) {
268
- this.cdkRef.removePanelClass(classes);
239
+ if (this.cdkRef) {
240
+ this.cdkRef.removePanelClass(classes);
241
+ }
242
+ else {
243
+ this.pendingPanelRemoves.push(...(Array.isArray(classes) ? classes : [classes]));
244
+ }
269
245
  return this;
270
246
  }
271
247
  closeWithOrigin(origin, result) {
@@ -273,7 +249,7 @@ class TwDialogRef {
273
249
  return;
274
250
  const predicate = this.config.closePredicate;
275
251
  if (predicate &&
276
- !predicate(result, this.config, this.componentInstance)) {
252
+ !predicate(result, this.config, this.componentInstanceValue)) {
277
253
  return;
278
254
  }
279
255
  this.pendingResult = result;
@@ -281,8 +257,15 @@ class TwDialogRef {
281
257
  this.stateSignal.set('closing');
282
258
  this.beforeClosedSubject.next(result);
283
259
  this.beforeClosedSubject.complete();
260
+ if (!this.cdkRef) {
261
+ // Closed before the render chunk attached: the overlay was never created,
262
+ // so there is nothing to animate or dispose. Synthesize the closed state
263
+ // and let the service skip opening entirely (it checks state()).
264
+ this.finishClose();
265
+ return;
266
+ }
284
267
  this.cdkRef.overlayRef.detachBackdrop();
285
- this.containerInstance._startExitAnimation();
268
+ this.container._startExitAnimation();
286
269
  }
287
270
  finishClose() {
288
271
  if (this.stateSignal() === 'closed')
@@ -290,25 +273,41 @@ class TwDialogRef {
290
273
  this.stateSignal.set('closed');
291
274
  this.afterClosedSubject.next(this.pendingResult);
292
275
  this.afterClosedSubject.complete();
293
- if (this.cdkRef.containerInstance) {
276
+ // Idempotent — a no-op if _setComponent already resolved with an instance.
277
+ this.resolveComponentReady(null);
278
+ this.backdropClickSubject.complete();
279
+ this.keydownEventsSubject.complete();
280
+ if (this.cdkRef?.containerInstance) {
294
281
  this.cdkRef.close(this.pendingResult, { focusOrigin: this.closeFocusOrigin });
295
282
  }
296
- this.componentInstance = null;
283
+ this.componentInstanceValue = null;
297
284
  }
298
285
  }
299
286
 
287
+ let nextDialogId = 0;
288
+ function generateDialogId() {
289
+ return `tw-dialog-${++nextDialogId}`;
290
+ }
300
291
  /**
301
292
  * Opens Tailwind-styled modal dialogs. Composes `@angular/cdk/dialog` for focus
302
293
  * trapping, portals, overlay plumbing, and adds a Tailwind container, richer
303
294
  * ref API, and animation lifecycle.
304
295
  *
296
+ * The rendering layer (`@angular/cdk/dialog` + the Tailwind container) is loaded
297
+ * through a dynamic `import()` on the first `open()` call, so merely registering
298
+ * this service costs nothing in the initial bundle. `open()` still returns its
299
+ * {@link TwDialogRef} synchronously — the dialog is rendered once the chunk
300
+ * lands. Read the rendered component via {@link TwDialogRef.whenComponentReady}.
301
+ *
305
302
  * Not `providedIn: 'root'` — register it via {@link provideTwDialog}.
306
303
  */
307
304
  class TwDialog {
308
- cdkDialog = inject(Dialog);
309
305
  injector = inject(Injector);
310
306
  defaultOptions = inject(TW_DIALOG_DEFAULT_OPTIONS, { optional: true }) ?? {};
311
307
  parentDialog = inject(TwDialog, { optional: true, skipSelf: true });
308
+ /** Cached renderer chunk import — kicked off on the first `open()`. */
309
+ rendererPromise = null;
310
+ destroyed = false;
312
311
  openDialogsAtThisLevel = signal([], /* @ts-ignore */
313
312
  ...(ngDevMode ? [{ debugName: "openDialogsAtThisLevel" }] : /* istanbul ignore next */ []));
314
313
  afterOpenedSubject = new Subject();
@@ -332,64 +331,25 @@ class TwDialog {
332
331
  * Opens a dialog using the given component or template.
333
332
  * @param content Component class or `TemplateRef`.
334
333
  * @param config Options merged over the application defaults.
335
- * @returns Reference controlling the opened dialog.
334
+ * @returns Reference controlling the opened dialog, returned synchronously.
336
335
  */
337
336
  open(content, config) {
338
337
  const merged = this.resolveConfig(config);
339
- let twRef;
340
- const cdkRef = this.cdkDialog.open(content, {
341
- id: merged.id,
342
- role: merged.role,
343
- data: merged.data,
344
- panelClass: merged.panelClass,
345
- backdropClass: merged.backdropClass ?? 'tw-dialog-backdrop',
346
- hasBackdrop: merged.hasBackdrop,
347
- width: merged.width,
348
- height: merged.height,
349
- minWidth: merged.minWidth,
350
- minHeight: merged.minHeight,
351
- maxWidth: merged.maxWidth,
352
- maxHeight: merged.maxHeight,
353
- direction: merged.direction,
354
- ariaDescribedBy: merged.ariaDescribedBy,
355
- ariaLabelledBy: merged.ariaLabelledBy,
356
- ariaLabel: merged.ariaLabel,
357
- ariaModal: merged.ariaModal,
358
- autoFocus: merged.autoFocus,
359
- restoreFocus: merged.restoreFocus,
360
- scrollStrategy: merged.scrollStrategy ?? this.resolveScrollStrategy(merged.scrollBehavior),
361
- closeOnNavigation: merged.closeOnNavigation,
362
- viewContainerRef: merged.viewContainerRef,
363
- injector: merged.injector,
364
- // We handle close/Escape/backdrop ourselves so we can run the exit animation.
365
- disableClose: true,
366
- closeOnOverlayDetachments: false,
367
- container: {
368
- type: DialogContainer,
369
- providers: () => [
370
- { provide: TwDialogConfig, useValue: merged },
371
- { provide: DialogConfig, useValue: merged },
372
- ],
373
- },
374
- providers: (_cdkRef, _cdkConfig, container) => {
375
- twRef = new TwDialogRef(_cdkRef, merged, container);
376
- const providers = [
377
- { provide: TwDialogRef, useValue: twRef },
378
- { provide: TW_DIALOG_DATA, useValue: merged.data ?? null },
379
- ];
380
- if (Array.isArray(merged.providers))
381
- providers.push(...merged.providers);
382
- return providers;
383
- },
384
- });
385
- // After CDK attaches the component, copy references onto our ref.
386
- twRef.componentRef = cdkRef.componentRef;
387
- twRef.componentInstance = cdkRef.componentInstance;
338
+ const id = merged.id ?? generateDialogId();
339
+ merged.id = id;
340
+ // Enforce id uniqueness eagerly. CDK throws this synchronously from
341
+ // `open()`; since our CDK open is now deferred, replicate the check here so
342
+ // the error still surfaces at the call site rather than in a later tick.
343
+ if (this.getDialogById(id)) {
344
+ throw new Error(`Dialog with id "${id}" exists already. The dialog id must be unique.`);
345
+ }
346
+ const twRef = new TwDialogRef(id, merged);
388
347
  const scope = this.parentDialog ?? this;
389
348
  scope.registerOpen(twRef);
390
349
  twRef.afterClosed().subscribe(() => {
391
350
  scope.unregister(twRef);
392
351
  });
352
+ void this.renderWhenReady(content, merged, twRef);
393
353
  return twRef;
394
354
  }
395
355
  /** Closes every open dialog managed by this service (and child services). */
@@ -403,12 +363,42 @@ class TwDialog {
403
363
  return this.openDialogs().find((dialog) => dialog.id === id);
404
364
  }
405
365
  ngOnDestroy() {
366
+ this.destroyed = true;
406
367
  const dialogs = [...this.openDialogsAtThisLevel()];
407
368
  for (let i = dialogs.length - 1; i >= 0; i--)
408
369
  dialogs[i].close();
409
370
  this.afterOpenedSubject.complete();
410
371
  this.afterAllClosedSubject.complete();
411
372
  }
373
+ /**
374
+ * @internal Resolves once the renderer chunk has loaded. Exposed for tests,
375
+ * which must await the dynamic import before asserting on rendered DOM.
376
+ */
377
+ async _whenRendered() {
378
+ await this.loadRenderer();
379
+ await Promise.resolve();
380
+ }
381
+ /**
382
+ * Render a dialog once the renderer chunk has loaded. The ref was already
383
+ * returned to the caller, so it may have been closed (or the service
384
+ * destroyed) while the import was in flight — both skip the actual open.
385
+ */
386
+ async renderWhenReady(content, merged, twRef) {
387
+ const openRendered = await this.loadRenderer();
388
+ if (!openRendered || this.destroyed)
389
+ return;
390
+ // Closed before the chunk landed — the facade already synthesized its
391
+ // closed state; never create the overlay.
392
+ if (twRef.state() === 'closed')
393
+ return;
394
+ openRendered(this.injector, content, merged, twRef);
395
+ }
396
+ loadRenderer() {
397
+ if (!this.rendererPromise) {
398
+ this.rendererPromise = import('./cdevhub-ngx-tw-dialog-dialog-renderer-DoIhoV3d.mjs').then(({ openRenderedDialog }) => this.destroyed ? null : openRenderedDialog);
399
+ }
400
+ return this.rendererPromise;
401
+ }
412
402
  registerOpen(ref) {
413
403
  if (this.parentDialog) {
414
404
  this.parentDialog.registerOpen(ref);
@@ -439,19 +429,6 @@ class TwDialog {
439
429
  Object.assign(merged, this.defaultOptions, config);
440
430
  return merged;
441
431
  }
442
- resolveScrollStrategy(strategy) {
443
- switch (strategy) {
444
- case 'close':
445
- return createCloseScrollStrategy(this.injector);
446
- case 'reposition':
447
- return createRepositionScrollStrategy(this.injector);
448
- case 'noop':
449
- return createNoopScrollStrategy();
450
- case 'block':
451
- default:
452
- return createBlockScrollStrategy(this.injector);
453
- }
454
- }
455
432
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: TwDialog, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
456
433
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: TwDialog });
457
434
  }
@@ -540,21 +517,16 @@ class DialogTitleDirective {
540
517
  dialogRef = inject(TwDialogRef, {
541
518
  optional: true,
542
519
  });
543
- // Ancestor-DI fallback for the rare case where `TwDialogRef` is not in the
544
- // directive's injector chain (e.g. heavily nested template portals). The
545
- // container ALWAYS resolves via element-injector traversal because the
546
- // directive lives inside `<tw-dialog-container>`'s DOM tree.
547
- container = inject(DialogContainer, { optional: true, skipSelf: true });
548
520
  /** Custom id for the title element. Defaults to a generated unique id. */
549
521
  id = input(this.generatedId, /* @ts-ignore */
550
522
  ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
551
523
  ngOnInit() {
552
- const containerInstance = this.dialogRef?.containerInstance ?? this.container;
524
+ const containerInstance = this.dialogRef?.containerInstance;
553
525
  if (containerInstance)
554
526
  containerInstance._addAriaLabelledBy(this.id());
555
527
  }
556
528
  ngOnDestroy() {
557
- const containerInstance = this.dialogRef?.containerInstance ?? this.container;
529
+ const containerInstance = this.dialogRef?.containerInstance;
558
530
  if (containerInstance)
559
531
  containerInstance._removeAriaLabelledBy(this.id());
560
532
  }
@@ -595,18 +567,16 @@ class DialogDescriptionDirective {
595
567
  dialogRef = inject(TwDialogRef, {
596
568
  optional: true,
597
569
  });
598
- // Ancestor-DI fallback — see DialogTitleDirective.
599
- container = inject(DialogContainer, { optional: true, skipSelf: true });
600
570
  /** Custom id for the description element. Defaults to a generated unique id. */
601
571
  id = input(this.generatedId, /* @ts-ignore */
602
572
  ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
603
573
  ngOnInit() {
604
- const containerInstance = this.dialogRef?.containerInstance ?? this.container;
574
+ const containerInstance = this.dialogRef?.containerInstance;
605
575
  if (containerInstance)
606
576
  containerInstance._addAriaDescribedBy(this.id());
607
577
  }
608
578
  ngOnDestroy() {
609
- const containerInstance = this.dialogRef?.containerInstance ?? this.container;
579
+ const containerInstance = this.dialogRef?.containerInstance;
610
580
  if (containerInstance)
611
581
  containerInstance._removeAriaDescribedBy(this.id());
612
582
  }
@@ -702,5 +672,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
702
672
  * Generated bundle index. Do not edit.
703
673
  */
704
674
 
705
- export { DialogActionsDirective, DialogCloseDirective, DialogContainer, DialogContentDirective, DialogDescriptionDirective, DialogHeaderDirective, DialogIconDirective, DialogSubtitleDirective, DialogTitleDirective, TW_DIALOG_DATA, TW_DIALOG_DEFAULT_OPTIONS, TwDialog, TwDialogConfig, TwDialogRef, provideTwDialog };
675
+ export { DialogActionsDirective, DialogCloseDirective, DialogContentDirective, DialogDescriptionDirective, DialogHeaderDirective, DialogIconDirective, DialogSubtitleDirective, DialogTitleDirective, TW_DIALOG_DATA, TW_DIALOG_DEFAULT_OPTIONS, TwDialog, TwDialogConfig, TwDialogRef, provideTwDialog };
706
676
  //# sourceMappingURL=cdevhub-ngx-tw-dialog.mjs.map