@radix-ng/primitives 1.0.0-beta.2 → 1.0.0-beta.4

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 (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +76 -6
  3. package/fesm2022/radix-ng-primitives-accordion.mjs +5 -3
  4. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  5. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +31 -24
  6. package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
  7. package/fesm2022/radix-ng-primitives-autocomplete.mjs +1744 -0
  8. package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -0
  9. package/fesm2022/radix-ng-primitives-calendar.mjs +5 -3
  10. package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
  11. package/fesm2022/radix-ng-primitives-combobox.mjs +1399 -606
  12. package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
  13. package/fesm2022/radix-ng-primitives-config.mjs +13 -4
  14. package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
  15. package/fesm2022/radix-ng-primitives-context-menu.mjs +51 -10
  16. package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
  17. package/fesm2022/radix-ng-primitives-core.mjs +1345 -64
  18. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  19. package/fesm2022/radix-ng-primitives-date-field.mjs +5 -3
  20. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  21. package/fesm2022/radix-ng-primitives-dialog.mjs +271 -145
  22. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  23. package/fesm2022/radix-ng-primitives-direction-provider.mjs +70 -0
  24. package/fesm2022/radix-ng-primitives-direction-provider.mjs.map +1 -0
  25. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +519 -184
  26. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
  27. package/fesm2022/radix-ng-primitives-drawer.mjs +154 -64
  28. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
  29. package/fesm2022/radix-ng-primitives-field.mjs +3 -2
  30. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
  31. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +517 -0
  32. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -0
  33. package/fesm2022/radix-ng-primitives-focus-scope.mjs +296 -70
  34. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  35. package/fesm2022/radix-ng-primitives-menu.mjs +894 -299
  36. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  37. package/fesm2022/radix-ng-primitives-menubar.mjs +32 -4
  38. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  39. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +176 -207
  40. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  41. package/fesm2022/radix-ng-primitives-popover.mjs +250 -250
  42. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  43. package/fesm2022/radix-ng-primitives-popper.mjs +94 -45
  44. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  45. package/fesm2022/radix-ng-primitives-portal.mjs +107 -17
  46. package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
  47. package/fesm2022/radix-ng-primitives-presence.mjs +262 -79
  48. package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
  49. package/fesm2022/radix-ng-primitives-preview-card.mjs +172 -218
  50. package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -1
  51. package/fesm2022/radix-ng-primitives-roving-focus.mjs +4 -2
  52. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  53. package/fesm2022/radix-ng-primitives-scroll-area.mjs +5 -4
  54. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  55. package/fesm2022/radix-ng-primitives-select.mjs +303 -234
  56. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  57. package/fesm2022/radix-ng-primitives-slider.mjs +5 -3
  58. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  59. package/fesm2022/radix-ng-primitives-stepper.mjs +5 -3
  60. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  61. package/fesm2022/radix-ng-primitives-time-field.mjs +5 -3
  62. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  63. package/fesm2022/radix-ng-primitives-toast.mjs +15 -36
  64. package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -1
  65. package/fesm2022/radix-ng-primitives-toggle-group.mjs +5 -3
  66. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  67. package/fesm2022/radix-ng-primitives-toolbar.mjs +5 -3
  68. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  69. package/fesm2022/radix-ng-primitives-tooltip.mjs +105 -145
  70. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  71. package/package.json +14 -1
  72. package/types/radix-ng-primitives-accordion.d.ts +4 -3
  73. package/types/radix-ng-primitives-alert-dialog.d.ts +17 -11
  74. package/types/radix-ng-primitives-autocomplete.d.ts +661 -0
  75. package/types/radix-ng-primitives-calendar.d.ts +5 -3
  76. package/types/radix-ng-primitives-combobox.d.ts +727 -293
  77. package/types/radix-ng-primitives-config.d.ts +1 -1
  78. package/types/radix-ng-primitives-context-menu.d.ts +15 -5
  79. package/types/radix-ng-primitives-core.d.ts +762 -14
  80. package/types/radix-ng-primitives-date-field.d.ts +3 -2
  81. package/types/radix-ng-primitives-dialog.d.ts +107 -55
  82. package/types/radix-ng-primitives-direction-provider.d.ts +41 -0
  83. package/types/radix-ng-primitives-dismissable-layer.d.ts +147 -99
  84. package/types/radix-ng-primitives-drawer.d.ts +49 -22
  85. package/types/radix-ng-primitives-field.d.ts +1 -0
  86. package/types/radix-ng-primitives-floating-focus-manager.d.ts +175 -0
  87. package/types/radix-ng-primitives-focus-scope.d.ts +132 -1
  88. package/types/radix-ng-primitives-menu.d.ts +204 -112
  89. package/types/radix-ng-primitives-navigation-menu.d.ts +61 -101
  90. package/types/radix-ng-primitives-popover.d.ts +82 -115
  91. package/types/radix-ng-primitives-popper.d.ts +46 -10
  92. package/types/radix-ng-primitives-portal.d.ts +53 -8
  93. package/types/radix-ng-primitives-presence.d.ts +98 -17
  94. package/types/radix-ng-primitives-preview-card.d.ts +63 -95
  95. package/types/radix-ng-primitives-roving-focus.d.ts +7 -6
  96. package/types/radix-ng-primitives-scroll-area.d.ts +2 -2
  97. package/types/radix-ng-primitives-select.d.ts +192 -158
  98. package/types/radix-ng-primitives-slider.d.ts +5 -4
  99. package/types/radix-ng-primitives-stepper.d.ts +4 -3
  100. package/types/radix-ng-primitives-time-field.d.ts +3 -2
  101. package/types/radix-ng-primitives-toast.d.ts +7 -7
  102. package/types/radix-ng-primitives-toggle-group.d.ts +5 -4
  103. package/types/radix-ng-primitives-toolbar.d.ts +3 -2
  104. package/types/radix-ng-primitives-tooltip.d.ts +48 -84
@@ -1,15 +1,15 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, booleanAttribute, inject, DestroyRef, model, input, output, signal, computed, effect, untracked, Directive, ElementRef, NgModule } from '@angular/core';
3
- import { createContext, useTransitionStatus, injectId, useScrollLock } from '@radix-ng/primitives/core';
2
+ import { InjectionToken, booleanAttribute, inject, DestroyRef, model, input, output, signal, computed, ElementRef, effect, untracked, Directive, Injector, afterNextRender, isDevMode, NgModule } from '@angular/core';
3
+ import * as i1 from '@radix-ng/primitives/core';
4
+ import { createContext, useTransitionStatus, injectId, createFloatingRootContext, createCancelableChangeEventDetails, provideFloatingTree, provideFloatingRootContext, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, useScrollLock, setupInternalBackdrop, RdxFloatingNodeRegistration, rdxDevError } from '@radix-ng/primitives/core';
4
5
  import { outputFromObservable, outputToObservable } from '@angular/core/rxjs-interop';
5
- import * as i1 from '@radix-ng/primitives/dismissable-layer';
6
- import { RdxDismissableLayer, provideRdxDismissableLayerConfig } from '@radix-ng/primitives/dismissable-layer';
7
- import * as i2 from '@radix-ng/primitives/focus-scope';
8
- import { RdxFocusScope, provideRdxFocusScopeConfig } from '@radix-ng/primitives/focus-scope';
6
+ import { RdxDismiss } from '@radix-ng/primitives/dismissable-layer';
7
+ import * as i2 from '@radix-ng/primitives/floating-focus-manager';
8
+ import { RdxFloatingFocusManager, provideFloatingFocusManagerConfig } from '@radix-ng/primitives/floating-focus-manager';
9
+ import { RdxFocusScope } from '@radix-ng/primitives/focus-scope';
9
10
  import * as i1$1 from '@radix-ng/primitives/portal';
10
- import { RdxPortal } from '@radix-ng/primitives/portal';
11
- import * as i1$2 from '@radix-ng/primitives/presence';
12
- import { provideRdxPresenceContext, RdxPresenceDirective } from '@radix-ng/primitives/presence';
11
+ import { RdxPortalPresence } from '@radix-ng/primitives/portal';
12
+ import { provideRdxPresenceContext } from '@radix-ng/primitives/presence';
13
13
 
14
14
  const DEFAULT_VARIANT = {
15
15
  role: 'dialog',
@@ -86,6 +86,8 @@ class RdxDialogRoot {
86
86
  this.triggers = signal([], ...(ngDevMode ? [{ debugName: "triggers" }] : /* istanbul ignore next */ []));
87
87
  this.payload = signal(undefined, ...(ngDevMode ? [{ debugName: "payload" }] : /* istanbul ignore next */ []));
88
88
  this.nestedOpenCount = signal(0, ...(ngDevMode ? [{ debugName: "nestedOpenCount" }] : /* istanbul ignore next */ []));
89
+ this.preventUnmountOnClose = signal(false, ...(ngDevMode ? [{ debugName: "preventUnmountOnClose" }] : /* istanbul ignore next */ []));
90
+ this.present = computed(() => this.open() || this.preventUnmountOnClose(), ...(ngDevMode ? [{ debugName: "present" }] : /* istanbul ignore next */ []));
89
91
  /** Whether this dialog is rendered inside another dialog. Fixed at construction. */
90
92
  this.nested = !!this.parentRoot;
91
93
  this.nestedDialogOpen = computed(() => this.nestedOpenCount() > 0, ...(ngDevMode ? [{ debugName: "nestedDialogOpen" }] : /* istanbul ignore next */ []));
@@ -95,6 +97,22 @@ class RdxDialogRoot {
95
97
  this.effectiveModal = computed(() => (this.variant.forceModal ? true : this.modal()), ...(ngDevMode ? [{ debugName: "effectiveModal" }] : /* istanbul ignore next */ []));
96
98
  /** Effective dismissal flag: disabled when the input asks, or when the variant forces it (alerts). */
97
99
  this.effectiveDisablePointerDismissal = computed(() => this.disablePointerDismissal() || this.variant.forcePointerDismissalDisabled, ...(ngDevMode ? [{ debugName: "effectiveDisablePointerDismissal" }] : /* istanbul ignore next */ []));
100
+ /**
101
+ * The shared per-popup floating context (ADR 0015 §1) — `open` mirrors the dialog's open state, the
102
+ * trigger registry is bridged from {@link registerTrigger}, and the reference / floating elements are
103
+ * set by the trigger / popup. The new dismissal + focus engines read this once the popup migrates.
104
+ */
105
+ this.floatingContext = createFloatingRootContext({
106
+ ownerDocument: inject(ElementRef).nativeElement.ownerDocument,
107
+ open: () => this.open()
108
+ });
109
+ // Keep the floating context's reference element in sync with the active trigger.
110
+ effect(() => this.floatingContext.setReferenceElement(this.trigger() ?? null));
111
+ effect(() => {
112
+ if (this.open() && this.preventUnmountOnClose()) {
113
+ this.preventUnmountOnClose.set(false);
114
+ }
115
+ });
98
116
  let previousOpen = this.open();
99
117
  effect(() => {
100
118
  const defaultOpen = this.defaultOpen();
@@ -136,29 +154,51 @@ class RdxDialogRoot {
136
154
  });
137
155
  }
138
156
  show(trigger = this.trigger(), payload, triggerId, reason = 'none', event = new Event('dialog.open-change')) {
157
+ const shouldAdoptPayload = trigger !== undefined || payload !== undefined;
158
+ if (this.open()) {
159
+ if (trigger) {
160
+ this.trigger.set(trigger);
161
+ }
162
+ if (triggerId !== undefined) {
163
+ this.triggerId.set(triggerId);
164
+ }
165
+ // Only adopt the payload when a trigger context is actually provided, so a bare
166
+ // imperative re-show on an already-open dialog doesn't clobber the live payload.
167
+ if (shouldAdoptPayload) {
168
+ this.payload.set(payload);
169
+ }
170
+ return;
171
+ }
172
+ const change = this.createOpenChangeEvent(true, reason, event, trigger, triggerId ?? this.triggerId());
173
+ this.onOpenChange.emit(change.payload);
174
+ if (change.eventDetails.isCanceled()) {
175
+ return;
176
+ }
139
177
  if (trigger) {
140
178
  this.trigger.set(trigger);
141
179
  }
142
180
  if (triggerId !== undefined) {
143
181
  this.triggerId.set(triggerId);
144
182
  }
145
- // Only adopt the payload when a trigger context is actually provided, so a bare
146
- // imperative re-show on an already-open dialog doesn't clobber the live payload.
147
- if (trigger !== undefined || payload !== undefined) {
183
+ if (shouldAdoptPayload) {
148
184
  this.payload.set(payload);
149
185
  }
150
- if (this.open()) {
151
- return;
152
- }
186
+ this.preventUnmountOnClose.set(false);
153
187
  this.open.set(true);
154
- this.emitOpenChange(true, reason, event);
188
+ this.floatingContext.events.emit('openchange', { open: true, reason, event: change.eventDetails.event });
155
189
  }
156
190
  close(reason = 'none', event = new Event('dialog.open-change')) {
157
191
  if (!this.open()) {
158
192
  return;
159
193
  }
194
+ const change = this.createOpenChangeEvent(false, reason, event, this.trigger(), this.triggerId());
195
+ this.onOpenChange.emit(change.payload);
196
+ if (change.eventDetails.isCanceled()) {
197
+ return;
198
+ }
199
+ this.preventUnmountOnClose.set(change.shouldPreventUnmountOnClose());
160
200
  this.open.set(false);
161
- this.emitOpenChange(false, reason, event);
201
+ this.floatingContext.events.emit('openchange', { open: false, reason, event: change.eventDetails.event });
162
202
  }
163
203
  toggle(triggerId, trigger, payload, event = new Event('dialog.open-change')) {
164
204
  if (this.open() && this.trigger() === trigger) {
@@ -170,6 +210,9 @@ class RdxDialogRoot {
170
210
  registerTrigger(id, trigger, payload) {
171
211
  this.registeredTriggers.set(id, { element: trigger, payload });
172
212
  this.triggers.update((triggers) => (triggers.includes(trigger) ? triggers : [...triggers, trigger]));
213
+ // Bridge into the floating context's trigger registry — the new dismissal/focus engines read it
214
+ // for inside-element checks (a press/focus on the trigger counts as inside, ADR 0015 §2).
215
+ this.floatingContext.triggers.add(trigger);
173
216
  if (this.triggerId() === id || (!this.trigger() && this.triggerId() === null)) {
174
217
  this.trigger.set(trigger);
175
218
  this.payload.set(payload());
@@ -179,6 +222,7 @@ class RdxDialogRoot {
179
222
  this.registeredTriggers.delete(id);
180
223
  }
181
224
  this.triggers.update((triggers) => triggers.filter((candidate) => candidate !== trigger));
225
+ this.floatingContext.triggers.delete(trigger);
182
226
  if (!this.destroyRef.destroyed && this.trigger() === trigger) {
183
227
  const next = this.registeredTriggers.entries().next().value;
184
228
  if (this.triggerId() !== null) {
@@ -209,14 +253,20 @@ class RdxDialogRoot {
209
253
  this.payload.set(trigger.payload());
210
254
  }
211
255
  }
212
- emitOpenChange(open, reason, event) {
213
- this.onOpenChange.emit({
214
- open,
215
- triggerId: this.triggerId(),
216
- trigger: this.trigger(),
217
- reason,
218
- event
219
- });
256
+ createOpenChangeEvent(open, reason, event, trigger, triggerId) {
257
+ const change = createCancelableChangeEventDetails(reason, event, trigger);
258
+ return {
259
+ eventDetails: change.eventDetails,
260
+ shouldPreventUnmountOnClose: change.shouldPreventUnmountOnClose,
261
+ payload: {
262
+ open,
263
+ triggerId,
264
+ trigger: change.eventDetails.trigger,
265
+ reason: change.eventDetails.reason,
266
+ event: change.eventDetails.event,
267
+ eventDetails: change.eventDetails
268
+ }
269
+ };
220
270
  }
221
271
  emitOpenChangeComplete(open) {
222
272
  if (!this.destroyRef.destroyed) {
@@ -224,14 +274,26 @@ class RdxDialogRoot {
224
274
  }
225
275
  }
226
276
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
227
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxDialogRoot, isStandalone: true, selector: "[rdxDialogRoot]", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaultOpen: { classPropertyName: "defaultOpen", publicName: "defaultOpen", isSignal: true, isRequired: false, transformFunction: null }, triggerId: { classPropertyName: "triggerId", publicName: "triggerId", isSignal: true, isRequired: false, transformFunction: null }, defaultTriggerId: { classPropertyName: "defaultTriggerId", publicName: "defaultTriggerId", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, disablePointerDismissal: { classPropertyName: "disablePointerDismissal", publicName: "disablePointerDismissal", isSignal: true, isRequired: false, transformFunction: null }, handle: { classPropertyName: "handle", publicName: "handle", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", triggerId: "triggerIdChange", onOpenChange: "onOpenChange", onOpenChangeComplete: "onOpenChangeComplete" }, providers: [provideRdxDialogRootContext(context)], exportAs: ["rdxDialogRoot"], ngImport: i0 }); }
277
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxDialogRoot, isStandalone: true, selector: "[rdxDialogRoot]", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaultOpen: { classPropertyName: "defaultOpen", publicName: "defaultOpen", isSignal: true, isRequired: false, transformFunction: null }, triggerId: { classPropertyName: "triggerId", publicName: "triggerId", isSignal: true, isRequired: false, transformFunction: null }, defaultTriggerId: { classPropertyName: "defaultTriggerId", publicName: "defaultTriggerId", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, disablePointerDismissal: { classPropertyName: "disablePointerDismissal", publicName: "disablePointerDismissal", isSignal: true, isRequired: false, transformFunction: null }, handle: { classPropertyName: "handle", publicName: "handle", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", triggerId: "triggerIdChange", onOpenChange: "onOpenChange", onOpenChangeComplete: "onOpenChangeComplete" }, providers: [
278
+ provideRdxDialogRootContext(context),
279
+ // New floating foundation (ADR 0015/0017 migration). Inherit-or-create tree so a nested dialog
280
+ // shares its parent's tree; the per-popup root context bridges open / triggers / reference.
281
+ provideFloatingTree(),
282
+ provideFloatingRootContext(() => inject(RdxDialogRoot).floatingContext)
283
+ ], exportAs: ["rdxDialogRoot"], ngImport: i0 }); }
228
284
  }
229
285
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogRoot, decorators: [{
230
286
  type: Directive,
231
287
  args: [{
232
288
  selector: '[rdxDialogRoot]',
233
289
  exportAs: 'rdxDialogRoot',
234
- providers: [provideRdxDialogRootContext(context)]
290
+ providers: [
291
+ provideRdxDialogRootContext(context),
292
+ // New floating foundation (ADR 0015/0017 migration). Inherit-or-create tree so a nested dialog
293
+ // shares its parent's tree; the per-popup root context bridges open / triggers / reference.
294
+ provideFloatingTree(),
295
+ provideFloatingRootContext(() => inject(RdxDialogRoot).floatingContext)
296
+ ]
235
297
  }]
236
298
  }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], defaultOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultOpen", required: false }] }], triggerId: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerId", required: false }] }, { type: i0.Output, args: ["triggerIdChange"] }], defaultTriggerId: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultTriggerId", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], disablePointerDismissal: [{ type: i0.Input, args: [{ isSignal: true, alias: "disablePointerDismissal", required: false }] }], handle: [{ type: i0.Input, args: [{ isSignal: true, alias: "handle", required: false }] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
237
299
  function contextFor(root) {
@@ -240,6 +302,7 @@ function contextFor(root) {
240
302
  titleId: root.titleId.asReadonly(),
241
303
  descriptionId: root.descriptionId.asReadonly(),
242
304
  isOpen: root.open,
305
+ present: root.present,
243
306
  modal: root.effectiveModal,
244
307
  disablePointerDismissal: root.effectiveDisablePointerDismissal,
245
308
  role: root.role,
@@ -261,13 +324,27 @@ function contextFor(root) {
261
324
 
262
325
  /**
263
326
  * An overlay displayed beneath the dialog popup.
327
+ *
328
+ * Decorative-only, so it carries `role="presentation"` (Base UI `DialogBackdrop`). By default a **nested**
329
+ * dialog renders no backdrop — the parent's already dims the page; stacking a second one double-darkens
330
+ * and intercepts the parent's outside-press. Set `forceRender` to opt back in.
264
331
  */
265
332
  class RdxDialogBackdrop {
266
333
  constructor() {
267
334
  this.rootContext = injectRdxDialogRootContext();
335
+ /** Render the backdrop even for a nested dialog (off by default, matching Base UI). */
336
+ this.forceRender = input(false, { ...(ngDevMode ? { debugName: "forceRender" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
337
+ // The backdrop is a second portal root (a body sibling of the popup). It is registered as owned DOM
338
+ // footprint for primitive-specific checks, but it is not a marker/aria keep-set member.
339
+ const floatingContext = inject(RDX_FLOATING_ROOT_CONTEXT, { optional: true });
340
+ if (floatingContext) {
341
+ const host = inject(ElementRef).nativeElement;
342
+ floatingContext.addFloatingElement(host);
343
+ inject(DestroyRef).onDestroy(() => floatingContext.removeFloatingElement(host));
344
+ }
268
345
  }
269
346
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogBackdrop, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
270
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxDialogBackdrop, isStandalone: true, selector: "[rdxDialogBackdrop]", host: { properties: { "attr.data-closed": "rootContext.isOpen() ? undefined : \"\"", "attr.data-ending-style": "rootContext.transitionStatus() === \"ending\" ? \"\" : undefined", "attr.data-open": "rootContext.isOpen() ? \"\" : undefined", "attr.data-starting-style": "rootContext.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-state": "rootContext.isOpen() ? \"open\" : \"closed\"", "attr.data-nested": "rootContext.nested ? \"\" : undefined", "attr.data-nested-dialog-open": "rootContext.nestedDialogOpen() ? \"\" : undefined" } }, exportAs: ["rdxDialogBackdrop"], ngImport: i0 }); }
347
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxDialogBackdrop, isStandalone: true, selector: "[rdxDialogBackdrop]", inputs: { forceRender: { classPropertyName: "forceRender", publicName: "forceRender", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "presentation" }, properties: { "hidden": "rootContext.nested && !forceRender()", "attr.data-closed": "rootContext.isOpen() ? undefined : \"\"", "attr.data-ending-style": "rootContext.transitionStatus() === \"ending\" ? \"\" : undefined", "attr.data-open": "rootContext.isOpen() ? \"\" : undefined", "attr.data-starting-style": "rootContext.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-state": "rootContext.isOpen() ? \"open\" : \"closed\"", "attr.data-nested": "rootContext.nested ? \"\" : undefined", "attr.data-nested-dialog-open": "rootContext.nestedDialogOpen() ? \"\" : undefined" } }, exportAs: ["rdxDialogBackdrop"], ngImport: i0 }); }
271
348
  }
272
349
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogBackdrop, decorators: [{
273
350
  type: Directive,
@@ -275,6 +352,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
275
352
  selector: '[rdxDialogBackdrop]',
276
353
  exportAs: 'rdxDialogBackdrop',
277
354
  host: {
355
+ role: 'presentation',
356
+ '[hidden]': 'rootContext.nested && !forceRender()',
278
357
  '[attr.data-closed]': 'rootContext.isOpen() ? undefined : ""',
279
358
  '[attr.data-ending-style]': 'rootContext.transitionStatus() === "ending" ? "" : undefined',
280
359
  '[attr.data-open]': 'rootContext.isOpen() ? "" : undefined',
@@ -284,7 +363,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
284
363
  '[attr.data-nested-dialog-open]': 'rootContext.nestedDialogOpen() ? "" : undefined'
285
364
  }
286
365
  }]
287
- }] });
366
+ }], ctorParameters: () => [], propDecorators: { forceRender: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceRender", required: false }] }] } });
288
367
 
289
368
  /**
290
369
  * A button that closes the dialog.
@@ -332,111 +411,164 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
332
411
  }]
333
412
  }], ctorParameters: () => [] });
334
413
 
414
+ /** Composite navigation keys a Dialog popup keeps to itself, so they never reach an enclosing Menu / Composite. */
415
+ const COMPOSITE_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End']);
416
+ const DIALOG_INTERNAL_BACKDROP_ATTR = 'data-rdx-dialog-internal-backdrop';
335
417
  /**
336
418
  * A container for the dialog contents.
419
+ *
420
+ * **ADR 0015/0017 Phase-4 migration — Dialog is the PILOT cutover onto the new floating dismissal +
421
+ * focus engine. Browser-verified** by `apps/visual-regression/tests/dialog.behavior.spec.ts` (trap,
422
+ * initial / return focus, Escape / outside-press / focus-out dismissal, nested-Escape deepest-first,
423
+ * backdrop-not-marked).
424
+ *
425
+ * **Mapping (legacy → new):**
426
+ * - `RdxDismissableLayer` (legacy) → `RdxFloatingNodeRegistration` (registers the tree node) +
427
+ * `RdxDismiss` (Escape / outside-press; reads the root context + node).
428
+ * - `RdxFocusScope` (direct) → `RdxFloatingFocusManager` (composes the reworked focus scope; trap +
429
+ * markOthers + close-on-focus-out), driven by `provideFloatingFocusManagerConfig`.
430
+ * - `disableOutsidePointerEvents` → the focus manager's `inert` pass marks outside elements
431
+ * non-interactive for a modal (finding #4), scoped to siblings of the popup's ancestor chain instead
432
+ * of a global `body { pointer-events: none }` lock — so the popup needs no `pointer-events: auto`.
433
+ * - focus-out close moved from the dismissal capability (`focusOutside: () => false`) to the manager
434
+ * (`manager.focusOut`), per ADR 0017 §3.
435
+ * - `isEventOnTrigger` preventDefault → removed: the trigger is in `context.triggers`, so the engine
436
+ * treats a press/focus on it as **inside** (no close-then-reopen).
437
+ *
438
+ * **Parity notes:**
439
+ * - **Lifecycle split (resolved 2026-06-16):** `enabled` is `open || transitionStatus === 'ending'`, so
440
+ * the trap / return-focus machinery survives the exit animation, while the manager's marker + isolation
441
+ * passes additionally key off `open` and release at close-start (Base UI `markOthers` gating).
442
+ * - **`trap-focus` split:** the manager traps focus for `modal === true` and `'trap-focus'`, but applies
443
+ * real `inert` isolation only for `modal === true`, matching Base UI's public contract that
444
+ * `modal="trap-focus"` leaves outside pointer interaction enabled.
445
+ * - **`returnFocus` orchestration (resolved 2026-06-16):** the manager now owns the return-focus *target*
446
+ * via the focus scope's `returnFocus` config seam (the scope owns the *timing* — its queued post-unmount
447
+ * frame). Dialog leaves it at the default (`returnFocus: true` → return to the element focused before
448
+ * open), so behavior is unchanged; a consumer can now also pass `false` / an element / a callback.
337
449
  */
338
450
  class RdxDialogPopup {
339
451
  constructor() {
340
452
  this.rootContext = injectRdxDialogRootContext();
341
- this.dismissableLayer = inject(RdxDismissableLayer);
453
+ this.host = inject(ElementRef).nativeElement;
454
+ this.floatingContext = inject(RDX_FLOATING_ROOT_CONTEXT);
455
+ this.registration = inject(RDX_FLOATING_REGISTRATION, { optional: true });
456
+ this.focusManager = inject(RdxFloatingFocusManager);
342
457
  this.focusScope = inject(RdxFocusScope);
343
- this.dismissDetails = {
344
- reason: 'none',
345
- event: new Event('dialog.dismiss')
346
- };
347
- /**
348
- * Event handler called when the escape key is down. Can be prevented.
349
- */
350
- this.escapeKeyDown = outputFromObservable(outputToObservable(this.dismissableLayer.escapeKeyDown));
351
- /**
352
- * Event handler called when a pointerdown event happens outside of the popup. Can be prevented.
353
- */
354
- this.pointerDownOutside = outputFromObservable(outputToObservable(this.dismissableLayer.pointerDownOutside));
355
- /**
356
- * Event handler called when focus moves outside of the popup. Can be prevented.
357
- */
358
- this.focusOutside = outputFromObservable(outputToObservable(this.dismissableLayer.focusOutside));
359
- /**
360
- * Event handler called when an interaction happens outside of the popup. Can be prevented.
361
- */
362
- this.interactOutside = outputFromObservable(outputToObservable(this.dismissableLayer.interactOutside));
363
- /**
364
- * Event handler called before focus moves into the popup. Can be prevented.
365
- */
458
+ /** Event handler called when the escape key is down. Can be prevented. */
459
+ this.escapeKeyDown = output();
460
+ /** Event handler called when a pointerdown event happens outside of the popup. Can be prevented. */
461
+ this.pointerDownOutside = output();
462
+ /** Event handler called when focus moves outside of the popup. Can be prevented. */
463
+ this.focusOutside = output();
464
+ /** Event handler called when an interaction (pointer / focus) happens outside of the popup. */
465
+ this.interactOutside = output();
466
+ /** Event handler called before focus moves into the popup. Can be prevented. */
366
467
  this.openAutoFocus = outputFromObservable(outputToObservable(this.focusScope.mountAutoFocus));
367
- /**
368
- * Event handler called before focus returns after the popup is removed. Can be prevented.
369
- */
468
+ /** Event handler called before focus returns after the popup is removed. Can be prevented. */
370
469
  this.closeAutoFocus = outputFromObservable(outputToObservable(this.focusScope.unmountAutoFocus));
371
- useScrollLock(computed(() => this.rootContext.modal() === true && this.rootContext.isOpen()));
372
- const unregisterTransitionElement = this.rootContext.registerTransitionElement(inject(ElementRef).nativeElement);
373
- inject(DestroyRef).onDestroy(unregisterTransitionElement);
374
- this.dismissableLayer.pointerDownOutside.subscribe((event) => {
375
- this.dismissDetails = { reason: 'outside-press', event };
376
- // A pointerdown on the trigger is an "outside" press relative to the portaled popup.
377
- // Let the trigger's own click toggle the dialog instead of dismissing here (which would
378
- // close and then immediately reopen).
379
- if (this.isEventOnTrigger(event)) {
380
- event.preventDefault();
381
- }
470
+ // The popup element is this layer's floating element (inside-surface for containment checks).
471
+ this.floatingContext.setFloatingElement(this.host);
472
+ // Scroll lock follows Base UI (`open && modal === true`): released at close-start so the page is
473
+ // scrollable again as the exit animation plays. Background pointer/AT isolation is no longer a
474
+ // global body lock — the focus manager applies real `inert` to outside elements (finding #4).
475
+ useScrollLock(computed(() => this.rootContext.modal() === true && this.rootContext.isOpen()), {
476
+ referenceElement: () => this.host
382
477
  });
383
- this.dismissableLayer.focusOutside.subscribe((event) => {
384
- this.dismissDetails = { reason: 'focus-out', event };
385
- if (this.isEventOnTrigger(event)) {
386
- event.preventDefault();
478
+ const unregisterTransitionElement = this.rootContext.registerTransitionElement(this.host);
479
+ inject(DestroyRef).onDestroy(unregisterTransitionElement);
480
+ // Base UI always renders an internal backdrop for a fully modal dialog. It is invisible and exists
481
+ // even when consumers also render `rdxDialogBackdrop`: outside pointer events land on this owned
482
+ // target instead of being swallowed by inert page content.
483
+ const injector = inject(Injector);
484
+ afterNextRender(() => setupInternalBackdrop(this.host, injector, {
485
+ marker: DIALOG_INTERNAL_BACKDROP_ATTR,
486
+ isOpen: () => this.rootContext.isOpen(),
487
+ shouldRender: () => this.rootContext.modal() === true,
488
+ cutout: () => this.host.closest('[rdxDialogViewport]'),
489
+ passThrough: () => this.host.closest('[rdxDialogViewport]') !== null
490
+ }));
491
+ // Dismissal (Base UI Dialog outside-press policy): Escape always closes; an outside press closes
492
+ // only the **topmost** dialog (a parent with an open nested dialog never self-closes) and only when
493
+ // pointer dismissal is enabled. A fully modal dialog uses the internal backdrop and intentional
494
+ // outside-press timing (click, not pointerdown). Focus-out is owned by the focus manager (below).
495
+ new RdxDismiss(this.floatingContext, () => this.registration?.node() ?? null, {
496
+ escapeKey: () => true,
497
+ outsidePress: () => this.isTopmost() && !this.rootContext.disablePointerDismissal(),
498
+ outsidePressEvent: () => (this.rootContext.modal() === true ? 'intentional' : 'sloppy'),
499
+ focusOutside: () => false,
500
+ onEscapeKeyDown: (event) => this.escapeKeyDown.emit(event),
501
+ onPointerDownOutside: (event) => {
502
+ this.pointerDownOutside.emit(event);
503
+ this.interactOutside.emit(event);
504
+ },
505
+ onDismiss: (reason, event) => {
506
+ this.rootContext.close(reason === 'escape-key' ? 'escape-key' : 'outside-press', event);
387
507
  }
388
508
  });
389
- this.dismissableLayer.escapeKeyDown.subscribe((event) => {
390
- this.dismissDetails = { reason: 'escape-key', event };
391
- });
392
- this.dismissableLayer.dismiss.subscribe(() => {
393
- const { reason, event } = this.dismissDetails;
394
- this.dismissDetails = { reason: 'none', event: new Event('dialog.dismiss') };
395
- // When pointer dismissal is disabled, keep the dialog open on outside interactions.
396
- // Escape always closes (standard a11y behavior).
397
- if ((reason === 'outside-press' || reason === 'focus-out') && this.rootContext.disablePointerDismissal()) {
398
- return;
509
+ // Focus-out close (ADR 0017 §3) the manager emits when focus leaves a non-modal dialog to an
510
+ // unrelated node; re-expose as `focusOutside` (preventable) and close unless vetoed.
511
+ this.focusManager.focusOut.subscribe((event) => {
512
+ this.focusOutside.emit(event);
513
+ this.interactOutside.emit(event);
514
+ if (!event.defaultPrevented) {
515
+ this.rootContext.close('focus-out', event);
399
516
  }
400
- this.rootContext.close(reason, event);
401
517
  });
402
518
  }
403
- isEventOnTrigger(event) {
404
- const target = event.target;
405
- return !!target && this.rootContext.triggers().some((trigger) => trigger.contains(target));
519
+ /** This dialog is the topmost (deepest open) one — it has no open nested dialog above it. */
520
+ isTopmost() {
521
+ return this.rootContext.isOpen() && !this.rootContext.nestedDialogOpen();
522
+ }
523
+ /**
524
+ * Composite navigation keys (arrows / Home / End) are kept inside the dialog (Base UI `DialogPopup`):
525
+ * a dialog opened from inside a Menu / Menubar / Composite must not let an arrow press bubble out and
526
+ * move the outer collection's active item.
527
+ */
528
+ onKeyDown(event) {
529
+ if (COMPOSITE_KEYS.has(event.key)) {
530
+ event.stopPropagation();
531
+ }
406
532
  }
407
533
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogPopup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
408
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxDialogPopup, isStandalone: true, selector: "[rdxDialogPopup]", outputs: { escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside", focusOutside: "focusOutside", interactOutside: "interactOutside", openAutoFocus: "openAutoFocus", closeAutoFocus: "closeAutoFocus" }, host: { properties: { "attr.role": "rootContext.role", "attr.aria-modal": "rootContext.modal() === true ? \"true\" : undefined", "attr.aria-describedby": "rootContext.descriptionId()", "attr.aria-labelledby": "rootContext.titleId()", "attr.data-closed": "rootContext.isOpen() ? undefined : \"\"", "attr.data-ending-style": "rootContext.transitionStatus() === \"ending\" ? \"\" : undefined", "attr.data-open": "rootContext.isOpen() ? \"\" : undefined", "attr.data-starting-style": "rootContext.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-state": "rootContext.isOpen() ? \"open\" : \"closed\"", "attr.data-nested": "rootContext.nested ? \"\" : undefined", "attr.data-nested-dialog-open": "rootContext.nestedDialogOpen() ? \"\" : undefined", "id": "rootContext.contentId" } }, providers: [
409
- provideRdxDismissableLayerConfig(() => {
534
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxDialogPopup, isStandalone: true, selector: "[rdxDialogPopup]", outputs: { escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside", focusOutside: "focusOutside", interactOutside: "interactOutside", openAutoFocus: "openAutoFocus", closeAutoFocus: "closeAutoFocus" }, host: { listeners: { "keydown": "onKeyDown($event)" }, properties: { "attr.role": "rootContext.role", "attr.aria-modal": "rootContext.modal() === true ? \"true\" : undefined", "attr.aria-describedby": "rootContext.descriptionId()", "attr.aria-labelledby": "rootContext.titleId()", "attr.data-closed": "rootContext.isOpen() ? undefined : \"\"", "attr.data-ending-style": "rootContext.transitionStatus() === \"ending\" ? \"\" : undefined", "attr.data-open": "rootContext.isOpen() ? \"\" : undefined", "attr.data-starting-style": "rootContext.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-state": "rootContext.isOpen() ? \"open\" : \"closed\"", "attr.data-nested": "rootContext.nested ? \"\" : undefined", "attr.data-nested-dialog-open": "rootContext.nestedDialogOpen() ? \"\" : undefined", "id": "rootContext.contentId" } }, providers: [
535
+ provideFloatingFocusManagerConfig(() => {
410
536
  const rootContext = injectRdxDialogRootContext();
411
537
  return {
412
- disableOutsidePointerEvents: computed(() => rootContext.modal() === true)
413
- };
414
- }),
415
- provideRdxFocusScopeConfig(() => {
416
- const rootContext = injectRdxDialogRootContext();
417
- return {
418
- trapped: computed(() => rootContext.modal() === 'trap-focus' || rootContext.modal() === true)
538
+ // Trap for a modal or trap-focus dialog (Base UI `modal={modal !== false}`).
539
+ modal: () => rootContext.modal() === true || rootContext.modal() === 'trap-focus',
540
+ // Full modal blocks outside pointer interaction; `trap-focus` only traps focus.
541
+ inert: () => rootContext.modal() === true,
542
+ // Active for the whole MOUNTED lifetime — including an explicit
543
+ // `preventUnmountOnClose()` cycle after the exit transition — matching Base UI's
544
+ // `FloatingFocusManager disabled={!mounted}` (NOT `open`) for trap/return-focus.
545
+ // Marker + isolation are additionally gated on `open` inside the manager.
546
+ enabled: () => rootContext.present(),
547
+ closeOnFocusOut: () => !rootContext.disablePointerDismissal()
419
548
  };
420
549
  })
421
- ], exportAs: ["rdxDialogPopup"], hostDirectives: [{ directive: i1.RdxDismissableLayer }, { directive: i2.RdxFocusScope }], ngImport: i0 }); }
550
+ ], exportAs: ["rdxDialogPopup"], hostDirectives: [{ directive: i1.RdxFloatingNodeRegistration }, { directive: i2.RdxFloatingFocusManager }], ngImport: i0 }); }
422
551
  }
423
552
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogPopup, decorators: [{
424
553
  type: Directive,
425
554
  args: [{
426
555
  selector: '[rdxDialogPopup]',
427
556
  exportAs: 'rdxDialogPopup',
428
- hostDirectives: [RdxDismissableLayer, RdxFocusScope],
557
+ hostDirectives: [RdxFloatingNodeRegistration, RdxFloatingFocusManager],
429
558
  providers: [
430
- provideRdxDismissableLayerConfig(() => {
431
- const rootContext = injectRdxDialogRootContext();
432
- return {
433
- disableOutsidePointerEvents: computed(() => rootContext.modal() === true)
434
- };
435
- }),
436
- provideRdxFocusScopeConfig(() => {
559
+ provideFloatingFocusManagerConfig(() => {
437
560
  const rootContext = injectRdxDialogRootContext();
438
561
  return {
439
- trapped: computed(() => rootContext.modal() === 'trap-focus' || rootContext.modal() === true)
562
+ // Trap for a modal or trap-focus dialog (Base UI `modal={modal !== false}`).
563
+ modal: () => rootContext.modal() === true || rootContext.modal() === 'trap-focus',
564
+ // Full modal blocks outside pointer interaction; `trap-focus` only traps focus.
565
+ inert: () => rootContext.modal() === true,
566
+ // Active for the whole MOUNTED lifetime — including an explicit
567
+ // `preventUnmountOnClose()` cycle after the exit transition — matching Base UI's
568
+ // `FloatingFocusManager disabled={!mounted}` (NOT `open`) for trap/return-focus.
569
+ // Marker + isolation are additionally gated on `open` inside the manager.
570
+ enabled: () => rootContext.present(),
571
+ closeOnFocusOut: () => !rootContext.disablePointerDismissal()
440
572
  };
441
573
  })
442
574
  ],
@@ -452,69 +584,63 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
452
584
  '[attr.data-state]': 'rootContext.isOpen() ? "open" : "closed"',
453
585
  '[attr.data-nested]': 'rootContext.nested ? "" : undefined',
454
586
  '[attr.data-nested-dialog-open]': 'rootContext.nestedDialogOpen() ? "" : undefined',
455
- '[id]': 'rootContext.contentId'
587
+ '[id]': 'rootContext.contentId',
588
+ '(keydown)': 'onKeyDown($event)'
456
589
  }
457
590
  }]
458
591
  }], ctorParameters: () => [], propDecorators: { escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }], pointerDownOutside: [{ type: i0.Output, args: ["pointerDownOutside"] }], focusOutside: [{ type: i0.Output, args: ["focusOutside"] }], interactOutside: [{ type: i0.Output, args: ["interactOutside"] }], openAutoFocus: [{ type: i0.Output, args: ["openAutoFocus"] }], closeAutoFocus: [{ type: i0.Output, args: ["closeAutoFocus"] }] } });
459
592
 
460
593
  /**
461
- * Moves the dialog to a different part of the DOM.
594
+ * Structural directive that teleports the dialog content (backdrop + popup) into a container (default
595
+ * `document.body`) while the dialog is open, and keeps it mounted until the CSS exit `@keyframes` on
596
+ * every root element finish.
597
+ *
598
+ * Dialog has two root nodes (backdrop + popup), so use the explicit `<ng-template rdxDialogPortal>`
599
+ * form. Pass `[container]` to portal into a different element.
462
600
  */
463
601
  class RdxDialogPortal {
464
602
  constructor() {
465
- this.rootContext = injectRdxDialogRootContext();
466
603
  /**
467
- * Optional container to portal the content into. Defaults to `document.body`.
604
+ * Optional container to portal the content into. Defaults to `document.body`. Declared here (and
605
+ * forwarded to the composed {@link RdxPortalPresence}) so that the drawer and alert-dialog portals
606
+ * can re-expose it through their own `hostDirectives`.
468
607
  */
469
608
  this.container = input(...(ngDevMode ? [undefined, { debugName: "container" }] : /* istanbul ignore next */ []));
470
609
  }
471
610
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogPortal, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
472
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxDialogPortal, isStandalone: true, selector: "[rdxDialogPortal]", inputs: { container: { classPropertyName: "container", publicName: "container", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-closed": "rootContext.isOpen() ? undefined : \"\"", "attr.data-open": "rootContext.isOpen() ? \"\" : undefined", "attr.data-state": "rootContext.isOpen() ? \"open\" : \"closed\"" } }, exportAs: ["rdxDialogPortal"], hostDirectives: [{ directive: i1$1.RdxPortal, inputs: ["container", "container"] }], ngImport: i0 }); }
611
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxDialogPortal, isStandalone: true, selector: "ng-template[rdxDialogPortal]", inputs: { container: { classPropertyName: "container", publicName: "container", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideRdxPresenceContext(() => ({ present: injectRdxDialogRootContext().present }))], exportAs: ["rdxDialogPortal"], hostDirectives: [{ directive: i1$1.RdxPortalPresence, inputs: ["container", "container"] }], ngImport: i0 }); }
473
612
  }
474
613
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogPortal, decorators: [{
475
614
  type: Directive,
476
615
  args: [{
477
- selector: '[rdxDialogPortal]',
616
+ selector: 'ng-template[rdxDialogPortal]',
478
617
  exportAs: 'rdxDialogPortal',
479
- hostDirectives: [
480
- {
481
- directive: RdxPortal,
482
- inputs: ['container']
483
- }
484
- ],
485
- host: {
486
- '[attr.data-closed]': 'rootContext.isOpen() ? undefined : ""',
487
- '[attr.data-open]': 'rootContext.isOpen() ? "" : undefined',
488
- '[attr.data-state]': 'rootContext.isOpen() ? "open" : "closed"'
489
- }
618
+ hostDirectives: [{ directive: RdxPortalPresence, inputs: ['container'] }],
619
+ providers: [provideRdxPresenceContext(() => ({ present: injectRdxDialogRootContext().present }))]
490
620
  }]
491
621
  }], propDecorators: { container: [{ type: i0.Input, args: [{ isSignal: true, alias: "container", required: false }] }] } });
492
-
493
622
  /**
494
- * Mounts the portal while the dialog is open and waits for CSS exit keyframes before unmounting.
623
+ * Dev-mode guard: `rdxDialogPortal` used to be an attribute directive on a `<div>`. It is now
624
+ * structural, so the old `<div rdxDialogPortal>` markup would silently stop portaling — fail loudly
625
+ * instead.
495
626
  */
496
- class RdxDialogPortalPresence {
497
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogPortalPresence, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
498
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxDialogPortalPresence, isStandalone: true, selector: "ng-template[rdxDialogPortalPresence]", providers: [
499
- provideRdxPresenceContext(() => {
500
- const context = injectRdxDialogRootContext();
501
- return { present: context.isOpen };
502
- })
503
- ], hostDirectives: [{ directive: i1$2.RdxPresenceDirective }], ngImport: i0 }); }
627
+ class RdxDialogPortalMisuseGuard {
628
+ constructor() {
629
+ if (isDevMode()) {
630
+ rdxDevError('dialog/portal-on-element', '`rdxDialogPortal` is now a structural directive. ' +
631
+ 'Use `<ng-template rdxDialogPortal>` around the backdrop and popup. ' +
632
+ 'rdxDialogPortalPresence has been removed.', 'components/dialog');
633
+ }
634
+ }
635
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogPortalMisuseGuard, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
636
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxDialogPortalMisuseGuard, isStandalone: true, selector: "[rdxDialogPortal]:not(ng-template)", ngImport: i0 }); }
504
637
  }
505
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogPortalPresence, decorators: [{
638
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogPortalMisuseGuard, decorators: [{
506
639
  type: Directive,
507
640
  args: [{
508
- selector: 'ng-template[rdxDialogPortalPresence]',
509
- hostDirectives: [RdxPresenceDirective],
510
- providers: [
511
- provideRdxPresenceContext(() => {
512
- const context = injectRdxDialogRootContext();
513
- return { present: context.isOpen };
514
- })
515
- ]
641
+ selector: '[rdxDialogPortal]:not(ng-template)'
516
642
  }]
517
- }] });
643
+ }], ctorParameters: () => [] });
518
644
 
519
645
  /**
520
646
  * An accessible title for the dialog.
@@ -709,8 +835,8 @@ function createRdxDialogHandle() {
709
835
  const dialogImports = [
710
836
  RdxDialogRoot,
711
837
  RdxDialogTrigger,
712
- RdxDialogPortalPresence,
713
838
  RdxDialogPortal,
839
+ RdxDialogPortalMisuseGuard,
714
840
  RdxDialogBackdrop,
715
841
  RdxDialogViewport,
716
842
  RdxDialogPopup,
@@ -722,8 +848,8 @@ class RdxDialogModule {
722
848
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
723
849
  static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: RdxDialogModule, imports: [RdxDialogRoot,
724
850
  RdxDialogTrigger,
725
- RdxDialogPortalPresence,
726
851
  RdxDialogPortal,
852
+ RdxDialogPortalMisuseGuard,
727
853
  RdxDialogBackdrop,
728
854
  RdxDialogViewport,
729
855
  RdxDialogPopup,
@@ -731,8 +857,8 @@ class RdxDialogModule {
731
857
  RdxDialogDescription,
732
858
  RdxDialogClose], exports: [RdxDialogRoot,
733
859
  RdxDialogTrigger,
734
- RdxDialogPortalPresence,
735
860
  RdxDialogPortal,
861
+ RdxDialogPortalMisuseGuard,
736
862
  RdxDialogBackdrop,
737
863
  RdxDialogViewport,
738
864
  RdxDialogPopup,
@@ -753,5 +879,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
753
879
  * Generated bundle index. Do not edit.
754
880
  */
755
881
 
756
- export { RDX_DIALOG_VARIANT, RdxDialogBackdrop, RdxDialogClose, RdxDialogDescription, RdxDialogHandle, RdxDialogModule, RdxDialogPopup, RdxDialogPortal, RdxDialogPortalPresence, RdxDialogRoot, RdxDialogTitle, RdxDialogTrigger, RdxDialogViewport, createRdxDialogHandle, dialogImports, injectRdxDialogRootContext, provideRdxDialogRootContext, provideRdxDialogVariant };
882
+ export { RDX_DIALOG_VARIANT, RdxDialogBackdrop, RdxDialogClose, RdxDialogDescription, RdxDialogHandle, RdxDialogModule, RdxDialogPopup, RdxDialogPortal, RdxDialogPortalMisuseGuard, RdxDialogRoot, RdxDialogTitle, RdxDialogTrigger, RdxDialogViewport, createRdxDialogHandle, dialogImports, injectRdxDialogRootContext, provideRdxDialogRootContext, provideRdxDialogVariant };
757
883
  //# sourceMappingURL=radix-ng-primitives-dialog.mjs.map