ng-primitives 0.7.0 → 0.7.2

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.
Files changed (39) hide show
  1. package/dialog/config/dialog.config.d.ts +43 -0
  2. package/dialog/dialog/dialog-ref.d.ts +40 -0
  3. package/dialog/dialog/dialog.directive.d.ts +29 -21
  4. package/dialog/dialog/dialog.service.d.ts +66 -0
  5. package/dialog/dialog-description/dialog-description.directive.d.ts +19 -0
  6. package/dialog/dialog-description/dialog-description.token.d.ts +14 -0
  7. package/dialog/dialog-overlay/dialog-overlay.directive.d.ts +8 -0
  8. package/dialog/{dialog-panel/dialog-panel.token.d.ts → dialog-overlay/dialog-overlay.token.d.ts} +4 -4
  9. package/dialog/dialog-title/dialog-title.directive.d.ts +14 -4
  10. package/dialog/dialog-trigger/dialog-trigger.directive.d.ts +24 -0
  11. package/dialog/dialog-trigger/dialog-trigger.token.d.ts +14 -0
  12. package/dialog/index.d.ts +8 -3
  13. package/esm2022/date-picker/date-picker-row-render/date-picker-row-render.directive.mjs +1 -2
  14. package/esm2022/dialog/config/dialog.config.mjs +28 -0
  15. package/esm2022/dialog/dialog/dialog-ref.mjs +44 -0
  16. package/esm2022/dialog/dialog/dialog.directive.mjs +59 -27
  17. package/esm2022/dialog/dialog/dialog.service.mjs +202 -0
  18. package/esm2022/dialog/dialog-description/dialog-description.directive.mjs +46 -0
  19. package/esm2022/dialog/dialog-description/dialog-description.token.mjs +16 -0
  20. package/esm2022/dialog/dialog-overlay/dialog-overlay.directive.mjs +35 -0
  21. package/esm2022/dialog/dialog-overlay/dialog-overlay.token.mjs +16 -0
  22. package/esm2022/dialog/dialog-title/dialog-title.directive.mjs +22 -7
  23. package/esm2022/dialog/dialog-trigger/dialog-trigger.directive.mjs +61 -0
  24. package/esm2022/dialog/dialog-trigger/dialog-trigger.token.mjs +16 -0
  25. package/esm2022/dialog/index.mjs +8 -3
  26. package/esm2022/internal/disabled/disabled.mjs +2 -2
  27. package/esm2022/utils/signals/async.mjs +3 -1
  28. package/fesm2022/ng-primitives-date-picker.mjs +0 -1
  29. package/fesm2022/ng-primitives-date-picker.mjs.map +1 -1
  30. package/fesm2022/ng-primitives-dialog.mjs +495 -46
  31. package/fesm2022/ng-primitives-dialog.mjs.map +1 -1
  32. package/fesm2022/ng-primitives-internal.mjs +1 -1
  33. package/fesm2022/ng-primitives-internal.mjs.map +1 -1
  34. package/fesm2022/ng-primitives-utils.mjs +2 -0
  35. package/fesm2022/ng-primitives-utils.mjs.map +1 -1
  36. package/package.json +7 -7
  37. package/dialog/dialog-panel/dialog-panel.directive.d.ts +0 -5
  38. package/esm2022/dialog/dialog-panel/dialog-panel.directive.mjs +0 -24
  39. package/esm2022/dialog/dialog-panel/dialog-panel.token.mjs +0 -16
@@ -1,6 +1,58 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, Directive, input, booleanAttribute, contentChild } from '@angular/core';
3
- import { uniqueId } from 'ng-primitives/utils';
2
+ import { InjectionToken, inject, input, Directive, HostListener, isDevMode, Injector, Injectable, ViewContainerRef, ElementRef, booleanAttribute, signal } from '@angular/core';
3
+ import { uniqueId, onChange } from 'ng-primitives/utils';
4
+ import { hasModifierKey } from '@angular/cdk/keycodes';
5
+ import { Subject, defer } from 'rxjs';
6
+ import { FocusMonitor } from '@angular/cdk/a11y';
7
+ import * as i1 from 'ng-primitives/button';
8
+ import { NgpButton } from 'ng-primitives/button';
9
+ import { Overlay, OverlayContainer, OverlayConfig } from '@angular/cdk/overlay';
10
+ import { TemplatePortal } from '@angular/cdk/portal';
11
+ import { startWith } from 'rxjs/operators';
12
+ import * as i1$1 from 'ng-primitives/focus-trap';
13
+ import { NgpFocusTrap } from 'ng-primitives/focus-trap';
14
+
15
+ const defaultDialogConfig = {
16
+ role: 'dialog',
17
+ modal: true,
18
+ closeOnNavigation: true,
19
+ };
20
+ const NgpDialogConfigToken = new InjectionToken('NgpDialogConfigToken');
21
+ /**
22
+ * Provide the default Dialog configuration
23
+ * @param config The Dialog configuration
24
+ * @returns The provider
25
+ */
26
+ function provideDialogConfig(config) {
27
+ return [
28
+ {
29
+ provide: NgpDialogConfigToken,
30
+ useValue: { ...defaultDialogConfig, ...config },
31
+ },
32
+ ];
33
+ }
34
+ /**
35
+ * Inject the Dialog configuration
36
+ * @returns The global Dialog configuration
37
+ */
38
+ function injectDialogConfig() {
39
+ return inject(NgpDialogConfigToken, { optional: true }) ?? defaultDialogConfig;
40
+ }
41
+
42
+ /**
43
+ * Copyright © 2024 Angular Primitives.
44
+ * https://github.com/ng-primitives/ng-primitives
45
+ *
46
+ * This source code is licensed under the Apache 2.0 license found in the
47
+ * LICENSE file in the root directory of this source tree.
48
+ */
49
+ const NgpDialogToken = new InjectionToken('NgpDialogToken');
50
+ /**
51
+ * Inject the Dialog directive instance
52
+ */
53
+ function injectDialog() {
54
+ return inject(NgpDialogToken);
55
+ }
4
56
 
5
57
  /**
6
58
  * Copyright © 2024 Angular Primitives.
@@ -9,12 +61,12 @@ import { uniqueId } from 'ng-primitives/utils';
9
61
  * This source code is licensed under the Apache 2.0 license found in the
10
62
  * LICENSE file in the root directory of this source tree.
11
63
  */
12
- const NgpDialogPanelToken = new InjectionToken('NgpDialogPanelToken');
64
+ const NgpDialogDescriptionToken = new InjectionToken('NgpDialogDescriptionToken');
13
65
  /**
14
- * Inject the DialogPanel directive instance
66
+ * Inject the DialogDescription directive instance
15
67
  */
16
- function injectDialogPanel() {
17
- return inject(NgpDialogPanelToken);
68
+ function injectDialogDescription() {
69
+ return inject(NgpDialogDescriptionToken);
18
70
  }
19
71
 
20
72
  /**
@@ -24,19 +76,126 @@ function injectDialogPanel() {
24
76
  * This source code is licensed under the Apache 2.0 license found in the
25
77
  * LICENSE file in the root directory of this source tree.
26
78
  */
27
- class NgpDialogPanel {
28
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogPanel, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
29
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.1.1", type: NgpDialogPanel, isStandalone: true, selector: "[ngpDialogPanel]", providers: [{ provide: NgpDialogPanelToken, useExisting: NgpDialogPanel }], exportAs: ["ngpDialogPanel"], ngImport: i0 }); }
79
+ class NgpDialogDescription {
80
+ constructor() {
81
+ /** Access the dialog */
82
+ this.dialog = injectDialog();
83
+ /** The id of the descriptions. */
84
+ this.id = input(uniqueId('ngp-dialog-description'));
85
+ onChange(this.id, (id, prevId) => {
86
+ if (prevId) {
87
+ this.dialog.removeDescribedBy(prevId);
88
+ }
89
+ if (id) {
90
+ this.dialog.setDescribedBy(id);
91
+ }
92
+ });
93
+ }
94
+ ngOnDestroy() {
95
+ this.dialog.removeDescribedBy(this.id());
96
+ }
97
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogDescription, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
98
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.1.1", type: NgpDialogDescription, isStandalone: true, selector: "[ngpDialogDescription]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "id()" } }, providers: [{ provide: NgpDialogDescriptionToken, useExisting: NgpDialogDescription }], exportAs: ["ngpDialogDescription"], ngImport: i0 }); }
30
99
  }
31
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogPanel, decorators: [{
100
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogDescription, decorators: [{
32
101
  type: Directive,
33
102
  args: [{
34
103
  standalone: true,
35
- selector: '[ngpDialogPanel]',
36
- exportAs: 'ngpDialogPanel',
37
- providers: [{ provide: NgpDialogPanelToken, useExisting: NgpDialogPanel }],
104
+ selector: '[ngpDialogDescription]',
105
+ exportAs: 'ngpDialogDescription',
106
+ providers: [{ provide: NgpDialogDescriptionToken, useExisting: NgpDialogDescription }],
107
+ host: {
108
+ '[id]': 'id()',
109
+ },
38
110
  }]
39
- }] });
111
+ }], ctorParameters: () => [] });
112
+
113
+ /**
114
+ * Reference to a dialog opened via the Dialog service.
115
+ */
116
+ class NgpDialogRef {
117
+ constructor(overlayRef, config) {
118
+ this.overlayRef = overlayRef;
119
+ this.config = config;
120
+ /** Emits when the dialog has been closed. */
121
+ this.closed = new Subject();
122
+ this.keydownEvents = overlayRef.keydownEvents();
123
+ this.outsidePointerEvents = overlayRef.outsidePointerEvents();
124
+ this.id = config.id; // By the time the dialog is created we are guaranteed to have an ID.
125
+ this.keydownEvents.subscribe(event => {
126
+ if (event.key === 'Escape' && !this.disableClose && !hasModifierKey(event)) {
127
+ event.preventDefault();
128
+ this.close('keyboard');
129
+ }
130
+ });
131
+ this.detachSubscription = overlayRef.detachments().subscribe(() => this.close());
132
+ }
133
+ /**
134
+ * Close the dialog.
135
+ * @param result Optional result to return to the dialog opener.
136
+ * @param options Additional options to customize the closing behavior.
137
+ */
138
+ close(focusOrigin) {
139
+ this.overlayRef.dispose();
140
+ this.detachSubscription.unsubscribe();
141
+ this.closed.next(focusOrigin ?? null);
142
+ this.closed.complete();
143
+ }
144
+ /** Updates the position of the dialog based on the current position strategy. */
145
+ updatePosition() {
146
+ this.overlayRef.updatePosition();
147
+ return this;
148
+ }
149
+ }
150
+ function injectDialogRef() {
151
+ return inject(NgpDialogRef);
152
+ }
153
+
154
+ /**
155
+ * Copyright © 2024 Angular Primitives.
156
+ * https://github.com/ng-primitives/ng-primitives
157
+ *
158
+ * This source code is licensed under the Apache 2.0 license found in the
159
+ * LICENSE file in the root directory of this source tree.
160
+ */
161
+ const NgpDialogOverlayToken = new InjectionToken('NgpDialogOverlayToken');
162
+ /**
163
+ * Inject the DialogOverlay directive instance
164
+ */
165
+ function injectDialogOverlay() {
166
+ return inject(NgpDialogOverlayToken);
167
+ }
168
+
169
+ /**
170
+ * Copyright © 2024 Angular Primitives.
171
+ * https://github.com/ng-primitives/ng-primitives
172
+ *
173
+ * This source code is licensed under the Apache 2.0 license found in the
174
+ * LICENSE file in the root directory of this source tree.
175
+ */
176
+ class NgpDialogOverlay {
177
+ constructor() {
178
+ /** Access the dialog ref. */
179
+ this.dialogRef = injectDialogRef();
180
+ }
181
+ close() {
182
+ this.dialogRef.close();
183
+ }
184
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogOverlay, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
185
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.1.1", type: NgpDialogOverlay, isStandalone: true, selector: "[ngpDialogOverlay]", host: { listeners: { "click": "close()" } }, providers: [{ provide: NgpDialogOverlayToken, useExisting: NgpDialogOverlay }], exportAs: ["ngpDialogOverlay"], ngImport: i0 }); }
186
+ }
187
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogOverlay, decorators: [{
188
+ type: Directive,
189
+ args: [{
190
+ standalone: true,
191
+ selector: '[ngpDialogOverlay]',
192
+ exportAs: 'ngpDialogOverlay',
193
+ providers: [{ provide: NgpDialogOverlayToken, useExisting: NgpDialogOverlay }],
194
+ }]
195
+ }], propDecorators: { close: [{
196
+ type: HostListener,
197
+ args: ['click']
198
+ }] } });
40
199
 
41
200
  /**
42
201
  * Copyright © 2024 Angular Primitives.
@@ -62,13 +221,24 @@ function injectDialogTitle() {
62
221
  */
63
222
  class NgpDialogTitle {
64
223
  constructor() {
65
- /**
66
- * The id of the dialog title.
67
- */
224
+ /** Access the dialog. */
225
+ this.dialog = injectDialog();
226
+ /** The id of the title. */
68
227
  this.id = input(uniqueId('ngp-dialog-title'));
228
+ onChange(this.id, (id, prevId) => {
229
+ if (prevId) {
230
+ this.dialog.removeLabelledBy(prevId);
231
+ }
232
+ if (id) {
233
+ this.dialog.setLabelledBy(id);
234
+ }
235
+ });
236
+ }
237
+ ngOnDestroy() {
238
+ this.dialog.removeLabelledBy(this.id());
69
239
  }
70
240
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogTitle, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
71
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.1.1", type: NgpDialogTitle, isStandalone: true, selector: "[ngpDialogTitle]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: NgpDialogTitleToken, useExisting: NgpDialogTitle }], exportAs: ["ngpDialogTitle"], ngImport: i0 }); }
241
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.1.1", type: NgpDialogTitle, isStandalone: true, selector: "[ngpDialogTitle]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "id()" } }, providers: [{ provide: NgpDialogTitleToken, useExisting: NgpDialogTitle }], exportAs: ["ngpDialogTitle"], ngImport: i0 }); }
72
242
  }
73
243
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogTitle, decorators: [{
74
244
  type: Directive,
@@ -77,8 +247,204 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImpor
77
247
  selector: '[ngpDialogTitle]',
78
248
  exportAs: 'ngpDialogTitle',
79
249
  providers: [{ provide: NgpDialogTitleToken, useExisting: NgpDialogTitle }],
250
+ host: {
251
+ '[id]': 'id()',
252
+ },
253
+ }]
254
+ }], ctorParameters: () => [] });
255
+
256
+ /**
257
+ * Copyright © 2024 Angular Primitives.
258
+ * https://github.com/ng-primitives/ng-primitives
259
+ *
260
+ * This source code is licensed under the Apache 2.0 license found in the
261
+ * LICENSE file in the root directory of this source tree.
262
+ */
263
+ /**
264
+ * This is based on the Angular CDK Dialog service.
265
+ * https://github.com/angular/components/blob/main/src/cdk/dialog/dialog.ts
266
+ */
267
+ class NgpDialogManager {
268
+ constructor() {
269
+ this.overlay = inject(Overlay);
270
+ this.defaultOptions = injectDialogConfig();
271
+ this.parentDialogManager = inject(NgpDialogManager, {
272
+ optional: true,
273
+ skipSelf: true,
274
+ });
275
+ this.overlayContainer = inject(OverlayContainer);
276
+ this.scrollStrategy = this.defaultOptions.scrollStrategy ?? this.overlay.scrollStrategies.block();
277
+ this.openDialogsAtThisLevel = [];
278
+ this.afterAllClosedAtThisLevel = new Subject();
279
+ this.afterOpenedAtThisLevel = new Subject();
280
+ this.ariaHiddenElements = new Map();
281
+ /**
282
+ * Stream that emits when all open dialog have finished closing.
283
+ * Will emit on subscribe if there are no open dialogs to begin with.
284
+ */
285
+ this.afterAllClosed = defer(() => this.openDialogs.length
286
+ ? this.getAfterAllClosed()
287
+ : this.getAfterAllClosed().pipe(startWith(undefined)));
288
+ }
289
+ /** Keeps track of the currently-open dialogs. */
290
+ get openDialogs() {
291
+ return this.parentDialogManager
292
+ ? this.parentDialogManager.openDialogs
293
+ : this.openDialogsAtThisLevel;
294
+ }
295
+ /** Stream that emits when a dialog has been opened. */
296
+ get afterOpened() {
297
+ return this.parentDialogManager
298
+ ? this.parentDialogManager.afterOpened
299
+ : this.afterOpenedAtThisLevel;
300
+ }
301
+ /**
302
+ * Opens a modal dialog containing the given template.
303
+ */
304
+ open(templateRef, config) {
305
+ const defaults = this.defaultOptions;
306
+ config = { ...defaults, ...config };
307
+ config.id = config.id ?? uniqueId('ngp-dialog');
308
+ if (config.id && this.getDialogById(config.id) && isDevMode()) {
309
+ throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`);
310
+ }
311
+ const overlayConfig = this.getOverlayConfig(config);
312
+ const overlayRef = this.overlay.create(overlayConfig);
313
+ const dialogRef = new NgpDialogRef(overlayRef, config);
314
+ const injector = this.createInjector(config, dialogRef, undefined);
315
+ const context = {
316
+ $implicit: dialogRef,
317
+ close: dialogRef.close.bind(dialogRef),
318
+ };
319
+ overlayRef.attach(new TemplatePortal(templateRef, config.viewContainerRef, context, injector));
320
+ // If this is the first dialog that we're opening, hide all the non-overlay content.
321
+ if (!this.openDialogs.length) {
322
+ this.hideNonDialogContentFromAssistiveTechnology();
323
+ }
324
+ this.openDialogs.push(dialogRef);
325
+ dialogRef.closed.subscribe(() => this.removeOpenDialog(dialogRef, true));
326
+ this.afterOpened.next(dialogRef);
327
+ return dialogRef;
328
+ }
329
+ /**
330
+ * Closes all of the currently-open dialogs.
331
+ */
332
+ closeAll() {
333
+ reverseForEach(this.openDialogs, dialog => dialog.close());
334
+ }
335
+ /**
336
+ * Finds an open dialog by its id.
337
+ * @param id ID to use when looking up the dialog.
338
+ */
339
+ getDialogById(id) {
340
+ return this.openDialogs.find(dialog => dialog.id === id);
341
+ }
342
+ ngOnDestroy() {
343
+ // Make one pass over all the dialogs that need to be untracked, but should not be closed. We
344
+ // want to stop tracking the open dialog even if it hasn't been closed, because the tracking
345
+ // determines when `aria-hidden` is removed from elements outside the dialog.
346
+ reverseForEach(this.openDialogsAtThisLevel, dialog => {
347
+ // Check for `false` specifically since we want `undefined` to be interpreted as `true`.
348
+ this.removeOpenDialog(dialog, false);
349
+ });
350
+ // Make a second pass and close the remaining dialogs. We do this second pass in order to
351
+ // correctly dispatch the `afterAllClosed` event in case we have a mixed array of dialogs
352
+ // that should be closed and dialogs that should not.
353
+ reverseForEach(this.openDialogsAtThisLevel, dialog => dialog.close());
354
+ this.afterAllClosedAtThisLevel.complete();
355
+ this.afterOpenedAtThisLevel.complete();
356
+ this.openDialogsAtThisLevel = [];
357
+ }
358
+ /**
359
+ * Creates an overlay config from a dialog config.
360
+ */
361
+ getOverlayConfig(config) {
362
+ const state = new OverlayConfig({
363
+ positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),
364
+ scrollStrategy: config.scrollStrategy || this.scrollStrategy,
365
+ hasBackdrop: false,
366
+ disposeOnNavigation: config.closeOnNavigation,
367
+ });
368
+ return state;
369
+ }
370
+ /**
371
+ * Creates a custom injector to be used inside the dialog. This allows a component loaded inside
372
+ * of a dialog to close itself and, optionally, to return a value.
373
+ */
374
+ createInjector(config, dialogRef, fallbackInjector) {
375
+ const userInjector = config.injector || config.viewContainerRef?.injector;
376
+ const providers = [{ provide: NgpDialogRef, useValue: dialogRef }];
377
+ return Injector.create({ parent: userInjector || fallbackInjector, providers });
378
+ }
379
+ /**
380
+ * Removes a dialog from the array of open dialogs.
381
+ */
382
+ removeOpenDialog(dialogRef, emitEvent) {
383
+ const index = this.openDialogs.indexOf(dialogRef);
384
+ if (index > -1) {
385
+ this.openDialogs.splice(index, 1);
386
+ // If all the dialogs were closed, remove/restore the `aria-hidden`
387
+ // to a the siblings and emit to the `afterAllClosed` stream.
388
+ if (!this.openDialogs.length) {
389
+ this.ariaHiddenElements.forEach((previousValue, element) => {
390
+ if (previousValue) {
391
+ element.setAttribute('aria-hidden', previousValue);
392
+ }
393
+ else {
394
+ element.removeAttribute('aria-hidden');
395
+ }
396
+ });
397
+ this.ariaHiddenElements.clear();
398
+ if (emitEvent) {
399
+ this.getAfterAllClosed().next();
400
+ }
401
+ }
402
+ }
403
+ }
404
+ /** Hides all of the content that isn't an overlay from assistive technology. */
405
+ hideNonDialogContentFromAssistiveTechnology() {
406
+ const overlayContainer = this.overlayContainer.getContainerElement();
407
+ // Ensure that the overlay container is attached to the DOM.
408
+ if (overlayContainer.parentElement) {
409
+ const siblings = overlayContainer.parentElement.children;
410
+ for (let i = siblings.length - 1; i > -1; i--) {
411
+ const sibling = siblings[i];
412
+ if (sibling !== overlayContainer &&
413
+ sibling.nodeName !== 'SCRIPT' &&
414
+ sibling.nodeName !== 'STYLE' &&
415
+ !sibling.hasAttribute('aria-live')) {
416
+ this.ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden'));
417
+ sibling.setAttribute('aria-hidden', 'true');
418
+ }
419
+ }
420
+ }
421
+ }
422
+ getAfterAllClosed() {
423
+ const parent = this.parentDialogManager;
424
+ return parent ? parent.getAfterAllClosed() : this.afterAllClosedAtThisLevel;
425
+ }
426
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
427
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogManager, providedIn: 'root' }); }
428
+ }
429
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogManager, decorators: [{
430
+ type: Injectable,
431
+ args: [{
432
+ providedIn: 'root',
80
433
  }]
81
434
  }] });
435
+ /**
436
+ * Executes a callback against all elements in an array while iterating in reverse.
437
+ * Useful if the array is being modified as it is being iterated.
438
+ */
439
+ function reverseForEach(items, callback) {
440
+ let i = items.length;
441
+ while (i--) {
442
+ callback(items[i]);
443
+ }
444
+ }
445
+ function injectDialogManager() {
446
+ return inject(NgpDialogManager);
447
+ }
82
448
 
83
449
  /**
84
450
  * Copyright © 2024 Angular Primitives.
@@ -87,40 +453,118 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImpor
87
453
  * This source code is licensed under the Apache 2.0 license found in the
88
454
  * LICENSE file in the root directory of this source tree.
89
455
  */
90
- const NgpDialogToken = new InjectionToken('NgpDialogToken');
456
+ const NgpDialogTriggerToken = new InjectionToken('NgpDialogTriggerToken');
91
457
  /**
92
- * Inject the Dialog directive instance
458
+ * Inject the DialogTrigger directive instance
93
459
  */
94
- function injectDialog() {
95
- return inject(NgpDialogToken);
460
+ function injectDialogTrigger() {
461
+ return inject(NgpDialogTriggerToken);
96
462
  }
97
463
 
98
- class NgpDialog {
464
+ /**
465
+ * Copyright © 2024 Angular Primitives.
466
+ * https://github.com/ng-primitives/ng-primitives
467
+ *
468
+ * This source code is licensed under the Apache 2.0 license found in the
469
+ * LICENSE file in the root directory of this source tree.
470
+ */
471
+ class NgpDialogTrigger {
99
472
  constructor() {
100
- /**
101
- * Define the id of the dialog.
102
- */
103
- this.id = input(uniqueId('ngp-dialog'));
104
- /**
105
- * The open state of the dialog.
106
- */
107
- this.open = input(false, {
108
- alias: 'ngpDialogOpen',
109
- transform: booleanAttribute,
473
+ /** Access the dialog manager. */
474
+ this.dialogManager = inject(NgpDialogManager);
475
+ /** Access the view container ref. */
476
+ this.viewContainerRef = inject(ViewContainerRef);
477
+ /** Access the focus monitor. */
478
+ this.focusMonitor = inject(FocusMonitor);
479
+ /** Access the element ref. */
480
+ this.elementRef = inject(ElementRef);
481
+ /** The template to launch. */
482
+ this.template = input.required({
483
+ alias: 'ngpDialogTrigger',
110
484
  });
111
485
  /**
112
- * The type of the dialog.
486
+ * Store the dialog ref.
487
+ * @internal
113
488
  */
114
- this.type = input('dialog', {
489
+ this.dialogRef = null;
490
+ }
491
+ launch() {
492
+ this.dialogRef = this.dialogManager.open(this.template(), {
493
+ viewContainerRef: this.viewContainerRef,
494
+ });
495
+ this.dialogRef.closed.subscribe(focusOrigin => {
496
+ this.dialogRef = null;
497
+ // Focus the trigger element after the dialog closes.
498
+ this.focusMonitor.focusVia(this.elementRef.nativeElement, focusOrigin);
499
+ });
500
+ }
501
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
502
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.1.1", type: NgpDialogTrigger, isStandalone: true, selector: "[ngpDialogTrigger]", inputs: { template: { classPropertyName: "template", publicName: "ngpDialogTrigger", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "click": "launch()" } }, providers: [{ provide: NgpDialogTriggerToken, useExisting: NgpDialogTrigger }], exportAs: ["ngpDialogTrigger"], hostDirectives: [{ directive: i1.NgpButton }], ngImport: i0 }); }
503
+ }
504
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialogTrigger, decorators: [{
505
+ type: Directive,
506
+ args: [{
507
+ standalone: true,
508
+ selector: '[ngpDialogTrigger]',
509
+ exportAs: 'ngpDialogTrigger',
510
+ providers: [{ provide: NgpDialogTriggerToken, useExisting: NgpDialogTrigger }],
511
+ hostDirectives: [NgpButton],
512
+ }]
513
+ }], propDecorators: { launch: [{
514
+ type: HostListener,
515
+ args: ['click']
516
+ }] } });
517
+
518
+ class NgpDialog {
519
+ constructor() {
520
+ this.config = injectDialogConfig();
521
+ /** Access the dialog ref */
522
+ this.dialogRef = injectDialogRef();
523
+ /** The id of the dialog */
524
+ this.id = input(uniqueId('ngp-dialog'));
525
+ /** The dialog role. */
526
+ this.role = input(this.config.role, {
115
527
  alias: 'ngpDialogRole',
116
528
  });
117
- /**
118
- * Access the title of the dialog.
119
- */
120
- this.title = contentChild(NgpDialogTitleToken, { descendants: true });
529
+ /** Whether the dialog is a modal. */
530
+ this.modal = input(this.config.modal ?? false, {
531
+ alias: 'ngpDialogModal',
532
+ transform: booleanAttribute,
533
+ });
534
+ /** The labelledby ids */
535
+ this.labelledBy = signal([]);
536
+ /** The describedby ids */
537
+ this.describedBy = signal([]);
538
+ }
539
+ ngOnDestroy() {
540
+ this.close();
541
+ }
542
+ /** Close the dialog. */
543
+ close() {
544
+ this.dialogRef.close();
545
+ }
546
+ /** Stop click events from propagating to the overlay */
547
+ onClick(event) {
548
+ event.stopPropagation();
549
+ }
550
+ /** @internal register a labelledby id */
551
+ setLabelledBy(id) {
552
+ this.labelledBy.update(ids => [...ids, id]);
553
+ }
554
+ /** @internal register a describedby id */
555
+ setDescribedBy(id) {
556
+ this.describedBy.update(ids => [...ids, id]);
557
+ }
558
+ /** @internal remove a labelledby id */
559
+ removeLabelledBy(id) {
560
+ this.labelledBy.update(ids => ids.filter(i => i !== id));
561
+ }
562
+ /** @internal remove a describedby id */
563
+ removeDescribedBy(id) {
564
+ this.describedBy.update(ids => ids.filter(i => i !== id));
121
565
  }
122
566
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialog, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
123
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "18.1.1", type: NgpDialog, isStandalone: true, selector: "[ngpDialog]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "ngpDialogOpen", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "ngpDialogRole", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "dialog" }, properties: { "id": "id()", "attr.tabindex": "-1", "attr.data-open": "open()", "attr.aria-labelledby": "title() ?? null" } }, providers: [{ provide: NgpDialogToken, useExisting: NgpDialog }], queries: [{ propertyName: "title", first: true, predicate: NgpDialogTitleToken, descendants: true, isSignal: true }], exportAs: ["ngpDialog"], ngImport: i0 }); }
567
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.1.1", type: NgpDialog, isStandalone: true, selector: "[ngpDialog]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "ngpDialogRole", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "ngpDialogModal", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "tabindex": "-1" }, listeners: { "click": "onClick($event)" }, properties: { "id": "id()", "attr.role": "role()", "attr.aria-modal": "modal()", "attr.aria-labelledby": "labelledBy().join(\" \")", "attr.aria-describedby": "describedBy().join(\" \")" } }, providers: [{ provide: NgpDialogToken, useExisting: NgpDialog }], exportAs: ["ngpDialog"], hostDirectives: [{ directive: i1$1.NgpFocusTrap }], ngImport: i0 }); }
124
568
  }
125
569
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: NgpDialog, decorators: [{
126
570
  type: Directive,
@@ -129,15 +573,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImpor
129
573
  selector: '[ngpDialog]',
130
574
  exportAs: 'ngpDialog',
131
575
  providers: [{ provide: NgpDialogToken, useExisting: NgpDialog }],
576
+ hostDirectives: [NgpFocusTrap],
132
577
  host: {
133
- role: 'dialog',
578
+ tabindex: '-1',
134
579
  '[id]': 'id()',
135
- '[attr.tabindex]': '-1',
136
- '[attr.data-open]': 'open()',
137
- '[attr.aria-labelledby]': 'title() ?? null',
580
+ '[attr.role]': 'role()',
581
+ '[attr.aria-modal]': 'modal()',
582
+ '[attr.aria-labelledby]': 'labelledBy().join(" ")',
583
+ '[attr.aria-describedby]': 'describedBy().join(" ")',
138
584
  },
139
585
  }]
140
- }] });
586
+ }], propDecorators: { onClick: [{
587
+ type: HostListener,
588
+ args: ['click', ['$event']]
589
+ }] } });
141
590
 
142
591
  /**
143
592
  * Copyright © 2024 Angular Primitives.
@@ -151,5 +600,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImpor
151
600
  * Generated bundle index. Do not edit.
152
601
  */
153
602
 
154
- export { NgpDialog, NgpDialogPanel, NgpDialogPanelToken, NgpDialogTitle, NgpDialogTitleToken, NgpDialogToken };
603
+ export { NgpDialog, NgpDialogDescription, NgpDialogDescriptionToken, NgpDialogOverlay, NgpDialogOverlayToken, NgpDialogTitle, NgpDialogTitleToken, NgpDialogToken, NgpDialogTrigger, NgpDialogTriggerToken, provideDialogConfig };
155
604
  //# sourceMappingURL=ng-primitives-dialog.mjs.map