@radix-ng/primitives 1.0.0-beta.3 → 1.0.0-beta.5

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