@cdevhub/ngx-tw 0.3.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, createGlobalPositionStrategy } 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';
@@ -73,180 +69,22 @@ const SHEET_DATA = new InjectionToken('SHEET_DATA');
73
69
  /** Injection token for application-wide default sheet options. Set via `provideSheet()`. */
74
70
  const SHEET_DEFAULT_OPTIONS = new InjectionToken('SHEET_DEFAULT_OPTIONS');
75
71
 
76
- // Sheet uses a `data-[state]` + `data-[side]` driven CSS transition rather than
77
- // the project's `animate.enter`/`animate.leave` keyframes. Same rationale as
78
- // the dialog container (see `dialog-container.ts`):
79
- // 1. The ref owns the open/close lifecycle (state signal, observables) and
80
- // needs runtime-configurable enter/exit durations per `Sheet.open()` call
81
- // — `animate.enter` only accepts a static class name.
82
- // 2. The transform direction depends on `side`, and encoding four pairs of
83
- // keyframe classes (slide-in/out × 4 sides) is noisier than four
84
- // `data-[side=…]` selectors on a single transition rule.
85
- // All other library overlays (popover, menu, tooltip) use animate.enter/leave;
86
- // this divergence is intentional and isolated to the sheet container.
87
- const sheetContainerVariants = tv({
88
- slots: {
89
- host: 'fixed flex flex-col outline-none bg-surface-raised text-fg shadow-md overflow-hidden transition-[transform,opacity] ease-out motion-reduce:transition-none opacity-0 data-[state=open]:opacity-100',
90
- },
91
- variants: {
92
- side: {
93
- right: {
94
- host: 'h-screen right-0 top-0 border-l border-border data-[state=opening]:translate-x-full data-[state=closing]:translate-x-full data-[state=open]:translate-x-0',
95
- },
96
- left: {
97
- host: 'h-screen left-0 top-0 border-r border-border data-[state=opening]:-translate-x-full data-[state=closing]:-translate-x-full data-[state=open]:translate-x-0',
98
- },
99
- top: {
100
- host: 'w-screen left-0 top-0 border-b border-border data-[state=opening]:-translate-y-full data-[state=closing]:-translate-y-full data-[state=open]:translate-y-0',
101
- },
102
- bottom: {
103
- host: 'w-screen left-0 bottom-0 border-t border-border data-[state=opening]:translate-y-full data-[state=closing]:translate-y-full data-[state=open]:translate-y-0',
104
- },
105
- },
106
- size: {
107
- xs: { host: '' },
108
- sm: { host: '' },
109
- md: { host: '' },
110
- lg: { host: '' },
111
- xl: { host: '' },
112
- full: { host: '' },
113
- },
114
- },
115
- // Sheets are axis-aware: `size` controls width on horizontal sides and
116
- // height on vertical sides. Encoded as compound variants so a single
117
- // {side, size} pair resolves to one set of utility classes.
118
- compoundVariants: [
119
- // ── Right-anchored ─────────────────────────────────────────────
120
- { side: 'right', size: 'xs', class: { host: 'w-full max-w-xs' } },
121
- { side: 'right', size: 'sm', class: { host: 'w-full max-w-sm' } },
122
- { side: 'right', size: 'md', class: { host: 'w-full max-w-md' } },
123
- { side: 'right', size: 'lg', class: { host: 'w-full max-w-xl' } },
124
- { side: 'right', size: 'xl', class: { host: 'w-full max-w-2xl' } },
125
- { side: 'right', size: 'full', class: { host: 'w-screen max-w-none' } },
126
- // ── Left-anchored ──────────────────────────────────────────────
127
- { side: 'left', size: 'xs', class: { host: 'w-full max-w-xs' } },
128
- { side: 'left', size: 'sm', class: { host: 'w-full max-w-sm' } },
129
- { side: 'left', size: 'md', class: { host: 'w-full max-w-md' } },
130
- { side: 'left', size: 'lg', class: { host: 'w-full max-w-xl' } },
131
- { side: 'left', size: 'xl', class: { host: 'w-full max-w-2xl' } },
132
- { side: 'left', size: 'full', class: { host: 'w-screen max-w-none' } },
133
- // ── Top-anchored ───────────────────────────────────────────────
134
- { side: 'top', size: 'xs', class: { host: 'h-[20vh]' } },
135
- { side: 'top', size: 'sm', class: { host: 'h-[33vh]' } },
136
- { side: 'top', size: 'md', class: { host: 'h-[50vh]' } },
137
- { side: 'top', size: 'lg', class: { host: 'h-[66vh]' } },
138
- { side: 'top', size: 'xl', class: { host: 'h-[80vh]' } },
139
- { side: 'top', size: 'full', class: { host: 'h-screen' } },
140
- // ── Bottom-anchored ────────────────────────────────────────────
141
- { side: 'bottom', size: 'xs', class: { host: 'h-[20vh]' } },
142
- { side: 'bottom', size: 'sm', class: { host: 'h-[33vh]' } },
143
- { side: 'bottom', size: 'md', class: { host: 'h-[50vh]' } },
144
- { side: 'bottom', size: 'lg', class: { host: 'h-[66vh]' } },
145
- { side: 'bottom', size: 'xl', class: { host: 'h-[80vh]' } },
146
- { side: 'bottom', size: 'full', class: { host: 'h-screen' } },
147
- ],
148
- defaultVariants: { side: 'right', size: 'md' },
149
- }, { twMerge: true });
150
- /**
151
- * Sheet container rendered inside the CDK overlay. Wraps the user-provided
152
- * content in a Tailwind-styled, edge-anchored surface and coordinates slide
153
- * enter/exit transitions with {@link SheetRef}.
154
- *
155
- * The animation state machine, `aria-describedby` queue, and panel-class
156
- * merge are shared with `DialogContainer` via {@link OverlayContainerCoordinator}
157
- * (component-scoped — see `providers: [OverlayContainerCoordinator]`).
158
- *
159
- * @docs-private
160
- */
161
- class SheetContainer extends CdkDialogContainer {
162
- coordinator = inject(OverlayContainerCoordinator);
163
- /** Lifecycle state of the sheet's enter/exit animation. */
164
- state = this.coordinator.state;
165
- /** Emits whenever the animation state transitions. */
166
- animationStateChanged = this.coordinator.animationStateChanged;
167
- /** Resolved enter-animation duration in ms. */
168
- enterAnimationDuration;
169
- /** Resolved exit-animation duration in ms. */
170
- exitAnimationDuration;
171
- sideAttr = computed(() => this._config.side ?? 'right', /* @ts-ignore */
172
- ...(ngDevMode ? [{ debugName: "sideAttr" }] : /* istanbul ignore next */ []));
173
- variants = computed(() => {
174
- const side = this._config.side ?? 'right';
175
- const size = this._config.size ?? 'md';
176
- return sheetContainerVariants({ side, size });
177
- }, /* @ts-ignore */
178
- ...(ngDevMode ? [{ debugName: "variants" }] : /* istanbul ignore next */ []));
179
- hostClasses = computed(() => mergeOverlayPanelClass(this.variants().host(), this._config.panelClass), /* @ts-ignore */
180
- ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
181
- ariaDescribedByAttr = computed(() => this._config.ariaDescribedBy ||
182
- this.coordinator.describedByIds()[0] ||
183
- null, /* @ts-ignore */
184
- ...(ngDevMode ? [{ debugName: "ariaDescribedByAttr" }] : /* istanbul ignore next */ []));
185
- transitionDuration = this.coordinator.transitionDuration;
186
- constructor() {
187
- super();
188
- this.enterAnimationDuration = coerceOverlayDuration(this._config.enterAnimationDuration, 200);
189
- this.exitAnimationDuration = coerceOverlayDuration(this._config.exitAnimationDuration, 160);
190
- this.coordinator.setDurations(this.enterAnimationDuration, this.exitAnimationDuration);
191
- }
192
- _contentAttached() {
193
- super._contentAttached();
194
- this.coordinator.startEnterAnimation();
195
- }
196
- /** Triggered by the sheet ref to play the exit transition before disposing the overlay. */
197
- _startExitAnimation() {
198
- this.coordinator.startExitAnimation();
199
- }
200
- /** Registers a description id for the container's `aria-describedby`. */
201
- _addAriaDescribedBy(id) {
202
- this.coordinator.addAriaDescribedBy(id);
203
- }
204
- /** Removes a previously registered description id. */
205
- _removeAriaDescribedBy(id) {
206
- this.coordinator.removeAriaDescribedBy(id);
207
- }
208
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: SheetContainer, deps: [], target: i0.ɵɵFactoryTarget.Component });
209
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.7", type: SheetContainer, isStandalone: true, selector: "tw-sheet-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()", "attr.data-side": "sideAttr()", "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 });
210
- }
211
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: SheetContainer, decorators: [{
212
- type: Component,
213
- args: [{
214
- selector: 'tw-sheet-container',
215
- template: '<ng-template cdkPortalOutlet />',
216
- changeDetection: ChangeDetectionStrategy.OnPush,
217
- encapsulation: ViewEncapsulation.None,
218
- imports: [CdkPortalOutlet],
219
- providers: [OverlayContainerCoordinator],
220
- host: {
221
- tabindex: '-1',
222
- '[attr.id]': '_config.id || null',
223
- '[attr.role]': '_config.role',
224
- '[attr.aria-modal]': '_config.ariaModal',
225
- '[attr.aria-labelledby]': '_config.ariaLabel ? null : _ariaLabelledByQueue[0]',
226
- '[attr.aria-label]': '_config.ariaLabel',
227
- '[attr.aria-describedby]': 'ariaDescribedByAttr()',
228
- '[attr.data-state]': 'state()',
229
- '[attr.data-side]': 'sideAttr()',
230
- '[class]': 'hostClasses()',
231
- '[style.transition-duration.ms]': 'transitionDuration()',
232
- },
233
- }]
234
- }], ctorParameters: () => [] });
235
-
236
72
  /**
237
73
  * Reference to a sheet opened via {@link Sheet.open}. Drives the sheet
238
74
  * lifecycle (close, state, observables) and forwards useful overlay streams.
75
+ *
76
+ * The ref is returned **synchronously** from `open()`, but the sheet's render
77
+ * layer (`@angular/cdk/dialog` + the Tailwind container) is loaded through a
78
+ * dynamic `import()`. The ref therefore starts *detached*: `id`, `state`,
79
+ * `close()`, the lifecycle observables, and panel mutations all work
80
+ * immediately (mutations are buffered and replayed on attach), but the rendered
81
+ * component instance does not exist yet — read it via {@link whenComponentReady}
82
+ * instead of a synchronous `componentInstance` field.
239
83
  */
240
84
  class SheetRef {
241
- cdkRef;
242
85
  config;
243
- containerInstance;
244
- /** Unique ID of the sheet. */
86
+ /** Unique ID of the sheet. Generated eagerly by the service, so it is valid the instant `open()` returns. */
245
87
  id;
246
- /** Instance of the component rendered inside the sheet, or `null` for template sheets. */
247
- componentInstance = null;
248
- /** `ComponentRef` of the content component, or `null` for template sheets. */
249
- componentRef = null;
250
88
  /** Current lifecycle state. Reactively readable. */
251
89
  state;
252
90
  /** When `true`, close-via-escape AND close-via-backdrop are both disabled. */
@@ -260,19 +98,56 @@ class SheetRef {
260
98
  afterOpenedSubject = new ReplaySubject(1);
261
99
  beforeClosedSubject = new ReplaySubject(1);
262
100
  afterClosedSubject = new ReplaySubject(1);
101
+ // Facade-owned pass-throughs for the raw overlay streams, so a consumer that
102
+ // subscribes before the render chunk attaches still receives events once it
103
+ // does — the pre-deferral behaviour when `open()` wrapped a live `cdkRef`.
104
+ backdropClickSubject = new Subject();
105
+ keydownEventsSubject = new Subject();
106
+ cdkRef = null;
107
+ container = null;
108
+ componentInstanceValue = null;
109
+ componentRefValue = null;
110
+ resolveComponentReady;
111
+ componentReadyPromise = new Promise((resolve) => {
112
+ this.resolveComponentReady = resolve;
113
+ });
114
+ // Buffered mutations issued before the render chunk attached the CDK backend.
115
+ pendingPanelAdds = [];
116
+ pendingPanelRemoves = [];
263
117
  pendingResult;
264
118
  closeFocusOrigin;
265
- constructor(cdkRef, config, containerInstance) {
266
- this.cdkRef = cdkRef;
119
+ constructor(id, config) {
267
120
  this.config = config;
268
- this.containerInstance = containerInstance;
269
- this.id = cdkRef.id;
121
+ this.id = id;
270
122
  this.disableClose = config.disableClose;
271
123
  this.closeOnEscape = config.closeOnEscape;
272
124
  this.closeOnBackdropClick = config.closeOnBackdropClick;
273
125
  this.state = this.stateSignal.asReadonly();
126
+ }
127
+ /** The Tailwind container instance, or `null` until the sheet has attached. */
128
+ get containerInstance() {
129
+ return this.container;
130
+ }
131
+ /**
132
+ * @internal Wire this facade to its CDK backend. Called from the sheet
133
+ * renderer **inside** `cdkDialog.open()`'s `providers` callback — the same
134
+ * point the constructor ran before deferral — so subscriptions to
135
+ * `animationStateChanged` are in place before the container emits `'open'`.
136
+ */
137
+ _attach(cdkRef, container) {
138
+ this.cdkRef = cdkRef;
139
+ this.container = container;
140
+ // Forward the raw overlay streams into the facade pass-throughs.
141
+ cdkRef.backdropClick.subscribe(this.backdropClickSubject);
142
+ cdkRef.keydownEvents.subscribe(this.keydownEventsSubject);
274
143
  cdkRef.addPanelClass('tw-sheet-panel');
275
- const animationChanges = containerInstance.animationStateChanged;
144
+ for (const cls of this.pendingPanelAdds)
145
+ cdkRef.addPanelClass(cls);
146
+ for (const cls of this.pendingPanelRemoves)
147
+ cdkRef.removePanelClass(cls);
148
+ this.pendingPanelAdds.length = 0;
149
+ this.pendingPanelRemoves.length = 0;
150
+ const animationChanges = container.animationStateChanged;
276
151
  animationChanges
277
152
  .pipe(filter((event) => event.state === 'open'), take(1))
278
153
  .subscribe(() => {
@@ -301,6 +176,38 @@ class SheetRef {
301
176
  this.closeWithOrigin(event.type === 'keydown' ? 'keyboard' : 'mouse');
302
177
  });
303
178
  }
179
+ /** @internal Record the rendered component after `cdkDialog.open()` returns. */
180
+ _setComponent(instance, ref) {
181
+ this.componentInstanceValue = instance;
182
+ this.componentRefValue = ref;
183
+ this.resolveComponentReady(instance);
184
+ }
185
+ /**
186
+ * Resolves with the rendered content-component instance once the sheet's
187
+ * render chunk has loaded and attached. Resolves `null` for template sheets,
188
+ * and for a sheet closed before it ever opened.
189
+ *
190
+ * Replaces the former synchronous `componentInstance` field, which cannot be
191
+ * populated before the deferred render chunk lands.
192
+ *
193
+ * @example
194
+ * ```ts
195
+ * const ref = sheet.open(FilterSheet);
196
+ * const filters = await ref.whenComponentReady();
197
+ * filters?.reset();
198
+ * ```
199
+ */
200
+ whenComponentReady() {
201
+ return this.componentReadyPromise;
202
+ }
203
+ /** The rendered component instance, or `null` if not yet attached / a template sheet. Prefer {@link whenComponentReady}. */
204
+ get componentInstance() {
205
+ return this.componentInstanceValue;
206
+ }
207
+ /** The rendered `ComponentRef`, or `null` if not yet attached / a template sheet. */
208
+ get componentRef() {
209
+ return this.componentRefValue;
210
+ }
304
211
  /**
305
212
  * Closes the sheet. The exit slide animation runs before the overlay is disposed.
306
213
  * @param result Value forwarded to `afterClosed()` subscribers.
@@ -320,22 +227,32 @@ class SheetRef {
320
227
  afterClosed() {
321
228
  return this.afterClosedSubject.asObservable();
322
229
  }
323
- /** Backdrop click stream (emits even when `closeOnBackdropClick` / `disableClose` is set). */
230
+ /** Backdrop click stream (emits even when `closeOnBackdropClick` / `disableClose` is set). Buffered — a subscription made before the sheet attaches receives events once it does. */
324
231
  backdropClick() {
325
- return this.cdkRef.backdropClick;
232
+ return this.backdropClickSubject.asObservable();
326
233
  }
327
- /** Keydown event stream for the overlay. */
234
+ /** Keydown event stream for the overlay. Buffered — a subscription made before the sheet attaches receives events once it does. */
328
235
  keydownEvents() {
329
- return this.cdkRef.keydownEvents;
236
+ return this.keydownEventsSubject.asObservable();
330
237
  }
331
- /** Adds CSS classes to the overlay panel. */
238
+ /** Adds CSS classes to the overlay panel. Buffered until attach. */
332
239
  addPanelClass(classes) {
333
- this.cdkRef.addPanelClass(classes);
240
+ if (this.cdkRef) {
241
+ this.cdkRef.addPanelClass(classes);
242
+ }
243
+ else {
244
+ this.pendingPanelAdds.push(...(Array.isArray(classes) ? classes : [classes]));
245
+ }
334
246
  return this;
335
247
  }
336
- /** Removes CSS classes from the overlay panel. */
248
+ /** Removes CSS classes from the overlay panel. Buffered until attach. */
337
249
  removePanelClass(classes) {
338
- this.cdkRef.removePanelClass(classes);
250
+ if (this.cdkRef) {
251
+ this.cdkRef.removePanelClass(classes);
252
+ }
253
+ else {
254
+ this.pendingPanelRemoves.push(...(Array.isArray(classes) ? classes : [classes]));
255
+ }
339
256
  return this;
340
257
  }
341
258
  closeWithOrigin(origin, result) {
@@ -343,7 +260,7 @@ class SheetRef {
343
260
  return;
344
261
  const predicate = this.config.closePredicate;
345
262
  if (predicate &&
346
- !predicate(result, this.config, this.componentInstance)) {
263
+ !predicate(result, this.config, this.componentInstanceValue)) {
347
264
  return;
348
265
  }
349
266
  this.pendingResult = result;
@@ -351,8 +268,15 @@ class SheetRef {
351
268
  this.stateSignal.set('closing');
352
269
  this.beforeClosedSubject.next(result);
353
270
  this.beforeClosedSubject.complete();
271
+ if (!this.cdkRef) {
272
+ // Closed before the render chunk attached: the overlay was never created,
273
+ // so there is nothing to animate or dispose. Synthesize the closed state
274
+ // and let the service skip opening entirely (it checks state()).
275
+ this.finishClose();
276
+ return;
277
+ }
354
278
  this.cdkRef.overlayRef.detachBackdrop();
355
- this.containerInstance._startExitAnimation();
279
+ this.container._startExitAnimation();
356
280
  }
357
281
  finishClose() {
358
282
  if (this.stateSignal() === 'closed')
@@ -360,26 +284,42 @@ class SheetRef {
360
284
  this.stateSignal.set('closed');
361
285
  this.afterClosedSubject.next(this.pendingResult);
362
286
  this.afterClosedSubject.complete();
363
- if (this.cdkRef.containerInstance) {
287
+ // Idempotent — a no-op if _setComponent already resolved with an instance.
288
+ this.resolveComponentReady(null);
289
+ this.backdropClickSubject.complete();
290
+ this.keydownEventsSubject.complete();
291
+ if (this.cdkRef?.containerInstance) {
364
292
  this.cdkRef.close(this.pendingResult, { focusOrigin: this.closeFocusOrigin });
365
293
  }
366
- this.componentInstance = null;
294
+ this.componentInstanceValue = null;
367
295
  }
368
296
  }
369
297
 
298
+ let nextSheetId = 0;
299
+ function generateSheetId() {
300
+ return `tw-sheet-${++nextSheetId}`;
301
+ }
370
302
  /**
371
303
  * Opens edge-anchored sheet (drawer) overlays. Composes `@angular/cdk/dialog`
372
304
  * for focus trapping, portals, and overlay plumbing — adds a `GlobalPositionStrategy`
373
305
  * pinned to the requested viewport edge, a Tailwind container with axis-aware
374
306
  * sizing, slide enter/exit animations, and split close-behavior flags.
375
307
  *
308
+ * The rendering layer (`@angular/cdk/dialog` + the Tailwind container) is loaded
309
+ * through a dynamic `import()` on the first `open()` call, so merely registering
310
+ * this service costs nothing in the initial bundle. `open()` still returns its
311
+ * {@link SheetRef} synchronously — the sheet is rendered once the chunk lands.
312
+ * Read the rendered component via {@link SheetRef.whenComponentReady}.
313
+ *
376
314
  * Not `providedIn: 'root'` — register it via {@link provideSheet}.
377
315
  */
378
316
  class Sheet {
379
- cdkDialog = inject(Dialog);
380
317
  injector = inject(Injector);
381
318
  defaultOptions = inject(SHEET_DEFAULT_OPTIONS, { optional: true }) ?? {};
382
319
  parentSheet = inject(Sheet, { optional: true, skipSelf: true });
320
+ /** Cached renderer chunk import — kicked off on the first `open()`. */
321
+ rendererPromise = null;
322
+ destroyed = false;
383
323
  openSheetsAtThisLevel = signal([], /* @ts-ignore */
384
324
  ...(ngDevMode ? [{ debugName: "openSheetsAtThisLevel" }] : /* istanbul ignore next */ []));
385
325
  afterOpenedSubject = new Subject();
@@ -403,61 +343,25 @@ class Sheet {
403
343
  * Opens a sheet using the given component or template.
404
344
  * @param content Component class or `TemplateRef`.
405
345
  * @param config Options merged over the application defaults.
406
- * @returns Reference controlling the opened sheet.
346
+ * @returns Reference controlling the opened sheet, returned synchronously.
407
347
  */
408
348
  open(content, config) {
409
349
  const merged = this.resolveConfig(config);
410
- let sheetRef;
411
- const cdkRef = this.cdkDialog.open(content, {
412
- id: merged.id,
413
- role: merged.role,
414
- data: merged.data,
415
- panelClass: merged.panelClass,
416
- backdropClass: merged.backdropClass || 'tw-sheet-backdrop',
417
- hasBackdrop: merged.hasBackdrop,
418
- // Sheet sizing happens on the container element (axis-aware width/height
419
- // utilities). The overlay pane itself is the full-edge bounding box.
420
- direction: merged.direction,
421
- ariaDescribedBy: merged.ariaDescribedBy,
422
- ariaLabelledBy: merged.ariaLabelledBy,
423
- ariaLabel: merged.ariaLabel,
424
- ariaModal: merged.ariaModal,
425
- autoFocus: merged.autoFocus,
426
- restoreFocus: merged.restoreFocus,
427
- scrollStrategy: merged.scrollStrategy ?? this.resolveScrollStrategy(merged.scrollBehavior),
428
- positionStrategy: merged.positionStrategy ?? this.resolvePositionStrategy(merged.side ?? 'right'),
429
- closeOnNavigation: merged.closeOnNavigation,
430
- viewContainerRef: merged.viewContainerRef,
431
- injector: merged.injector,
432
- // We handle close/Escape/backdrop ourselves so we can run the exit slide animation.
433
- disableClose: true,
434
- closeOnOverlayDetachments: false,
435
- container: {
436
- type: SheetContainer,
437
- providers: () => [
438
- { provide: SheetConfig, useValue: merged },
439
- { provide: DialogConfig, useValue: merged },
440
- ],
441
- },
442
- providers: (_cdkRef, _cdkConfig, container) => {
443
- sheetRef = new SheetRef(_cdkRef, merged, container);
444
- const providers = [
445
- { provide: SheetRef, useValue: sheetRef },
446
- { provide: SHEET_DATA, useValue: merged.data ?? null },
447
- ];
448
- if (Array.isArray(merged.providers))
449
- providers.push(...merged.providers);
450
- return providers;
451
- },
452
- });
453
- // After CDK attaches the component, copy references onto our ref.
454
- sheetRef.componentRef = cdkRef.componentRef;
455
- sheetRef.componentInstance = cdkRef.componentInstance;
350
+ const id = merged.id ?? generateSheetId();
351
+ merged.id = id;
352
+ // Enforce id uniqueness eagerly. CDK throws this synchronously from
353
+ // `open()`; since our CDK open is now deferred, replicate the check here so
354
+ // the error still surfaces at the call site rather than in a later tick.
355
+ if (this.getSheetById(id)) {
356
+ throw new Error(`Sheet with id "${id}" exists already. The sheet id must be unique.`);
357
+ }
358
+ const sheetRef = new SheetRef(id, merged);
456
359
  const scope = this.parentSheet ?? this;
457
360
  scope.registerOpen(sheetRef);
458
361
  sheetRef.afterClosed().subscribe(() => {
459
362
  scope.unregister(sheetRef);
460
363
  });
364
+ void this.renderWhenReady(content, merged, sheetRef);
461
365
  return sheetRef;
462
366
  }
463
367
  /** Closes every open sheet managed by this service (and child services). */
@@ -471,12 +375,42 @@ class Sheet {
471
375
  return this.openSheets().find((sheet) => sheet.id === id);
472
376
  }
473
377
  ngOnDestroy() {
378
+ this.destroyed = true;
474
379
  const sheets = [...this.openSheetsAtThisLevel()];
475
380
  for (let i = sheets.length - 1; i >= 0; i--)
476
381
  sheets[i].close();
477
382
  this.afterOpenedSubject.complete();
478
383
  this.afterAllClosedSubject.complete();
479
384
  }
385
+ /**
386
+ * @internal Resolves once the renderer chunk has loaded. Exposed for tests,
387
+ * which must await the dynamic import before asserting on rendered DOM.
388
+ */
389
+ async _whenRendered() {
390
+ await this.loadRenderer();
391
+ await Promise.resolve();
392
+ }
393
+ /**
394
+ * Render a sheet once the renderer chunk has loaded. The ref was already
395
+ * returned to the caller, so it may have been closed (or the service
396
+ * destroyed) while the import was in flight — both skip the actual open.
397
+ */
398
+ async renderWhenReady(content, merged, sheetRef) {
399
+ const openRendered = await this.loadRenderer();
400
+ if (!openRendered || this.destroyed)
401
+ return;
402
+ // Closed before the chunk landed — the facade already synthesized its
403
+ // closed state; never create the overlay.
404
+ if (sheetRef.state() === 'closed')
405
+ return;
406
+ openRendered(this.injector, content, merged, sheetRef);
407
+ }
408
+ loadRenderer() {
409
+ if (!this.rendererPromise) {
410
+ this.rendererPromise = import('./cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs').then(({ openRenderedSheet }) => this.destroyed ? null : openRenderedSheet);
411
+ }
412
+ return this.rendererPromise;
413
+ }
480
414
  registerOpen(ref) {
481
415
  if (this.parentSheet) {
482
416
  this.parentSheet.registerOpen(ref);
@@ -507,33 +441,6 @@ class Sheet {
507
441
  Object.assign(merged, this.defaultOptions, config);
508
442
  return merged;
509
443
  }
510
- resolveScrollStrategy(strategy) {
511
- switch (strategy) {
512
- case 'close':
513
- return createCloseScrollStrategy(this.injector);
514
- case 'reposition':
515
- return createRepositionScrollStrategy(this.injector);
516
- case 'noop':
517
- return createNoopScrollStrategy();
518
- case 'block':
519
- default:
520
- return createBlockScrollStrategy(this.injector);
521
- }
522
- }
523
- resolvePositionStrategy(side) {
524
- const strategy = createGlobalPositionStrategy(this.injector);
525
- switch (side) {
526
- case 'left':
527
- return strategy.top('0').left('0').bottom('0');
528
- case 'top':
529
- return strategy.top('0').left('0').right('0');
530
- case 'bottom':
531
- return strategy.bottom('0').left('0').right('0');
532
- case 'right':
533
- default:
534
- return strategy.top('0').right('0').bottom('0');
535
- }
536
- }
537
444
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: Sheet, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
538
445
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: Sheet });
539
446
  }
@@ -620,21 +527,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
620
527
  class SheetTitleDirective {
621
528
  generatedId = inject(_IdGenerator).getId('tw-sheet-title-');
622
529
  sheetRef = inject(SheetRef, { optional: true });
623
- // Ancestor-DI fallback for the rare case where `SheetRef` is not in the
624
530
  // directive's injector chain (e.g. heavily nested template portals). The
625
531
  // container ALWAYS resolves via element-injector traversal because the
626
532
  // directive lives inside `<tw-sheet-container>`'s DOM tree.
627
- container = inject(SheetContainer, { optional: true, skipSelf: true });
628
533
  /** Custom id for the title element. Defaults to a generated unique id. */
629
534
  id = input(this.generatedId, /* @ts-ignore */
630
535
  ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
631
536
  ngOnInit() {
632
- const containerInstance = this.sheetRef?.containerInstance ?? this.container;
537
+ const containerInstance = this.sheetRef?.containerInstance;
633
538
  if (containerInstance)
634
539
  containerInstance._addAriaLabelledBy(this.id());
635
540
  }
636
541
  ngOnDestroy() {
637
- const containerInstance = this.sheetRef?.containerInstance ?? this.container;
542
+ const containerInstance = this.sheetRef?.containerInstance;
638
543
  if (containerInstance)
639
544
  containerInstance._removeAriaLabelledBy(this.id());
640
545
  }
@@ -673,18 +578,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
673
578
  class SheetDescriptionDirective {
674
579
  generatedId = inject(_IdGenerator).getId('tw-sheet-description-');
675
580
  sheetRef = inject(SheetRef, { optional: true });
676
- // Ancestor-DI fallback — see SheetTitleDirective.
677
- container = inject(SheetContainer, { optional: true, skipSelf: true });
678
581
  /** Custom id for the description element. Defaults to a generated unique id. */
679
582
  id = input(this.generatedId, /* @ts-ignore */
680
583
  ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
681
584
  ngOnInit() {
682
- const containerInstance = this.sheetRef?.containerInstance ?? this.container;
585
+ const containerInstance = this.sheetRef?.containerInstance;
683
586
  if (containerInstance)
684
587
  containerInstance._addAriaDescribedBy(this.id());
685
588
  }
686
589
  ngOnDestroy() {
687
- const containerInstance = this.sheetRef?.containerInstance ?? this.container;
590
+ const containerInstance = this.sheetRef?.containerInstance;
688
591
  if (containerInstance)
689
592
  containerInstance._removeAriaDescribedBy(this.id());
690
593
  }
@@ -778,5 +681,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
778
681
  * Generated bundle index. Do not edit.
779
682
  */
780
683
 
781
- export { SHEET_DATA, SHEET_DEFAULT_OPTIONS, Sheet, SheetActionsDirective, SheetCloseDirective, SheetConfig, SheetContainer, SheetContentDirective, SheetDescriptionDirective, SheetHeaderDirective, SheetIconDirective, SheetRef, SheetSubtitleDirective, SheetTitleDirective, provideSheet };
684
+ export { SHEET_DATA, SHEET_DEFAULT_OPTIONS, Sheet, SheetActionsDirective, SheetCloseDirective, SheetConfig, SheetContentDirective, SheetDescriptionDirective, SheetHeaderDirective, SheetIconDirective, SheetRef, SheetSubtitleDirective, SheetTitleDirective, provideSheet };
782
685
  //# sourceMappingURL=cdevhub-ngx-tw-sheet.mjs.map