@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
@@ -0,0 +1,803 @@
1
+ import { isPlatformBrowser } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { InjectionToken, booleanAttribute, signal, input, output, inject, computed, ElementRef, PLATFORM_ID, effect, DestroyRef, Directive } from '@angular/core';
4
+ import { RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION } from '@radix-ng/primitives/core';
5
+ import * as i1 from '@radix-ng/primitives/focus-scope';
6
+ import { provideRdxFocusScopeConfig, RdxFocusScopeConfigToken, useFocusGuardsTabbability, RdxFocusScope, FOCUS_GUARD_ATTR, getTabbableCandidates, focus, createFocusGuard, enableFocusInside, isOutsideEvent, getNextTabbable, getPreviousTabbable, composedContains, createAriaOwnsAnchor, getTabbableBeforeElement, getTabbableAfterElement } from '@radix-ng/primitives/focus-scope';
7
+
8
+ /** The neutral "outside the active floating layer" marker (Base UI `data-base-ui-inert`). */
9
+ const RDX_FLOATING_MARKER = 'data-rdx-floating-inert';
10
+ /** Per-element, per-attribute ref-counts. Keyed by element, so they are naturally per-`Document`. */
11
+ const controlCounters = {
12
+ inert: new WeakMap(),
13
+ 'aria-hidden': new WeakMap()
14
+ };
15
+ /** Elements that already carried the control attribute before we touched them — left in place on undo. */
16
+ const preExistingControlled = {
17
+ inert: new WeakSet(),
18
+ 'aria-hidden': new WeakSet()
19
+ };
20
+ let markerCounters = new WeakMap();
21
+ let lockCount = 0;
22
+ function unwrapHost(node) {
23
+ if (!node) {
24
+ return null;
25
+ }
26
+ return node instanceof ShadowRoot ? node.host : unwrapHost(node.parentNode);
27
+ }
28
+ /** Maps each target to the element actually inside `parent` (piercing shadow hosts), dropping the rest. */
29
+ function correctElements(parent, targets) {
30
+ return targets
31
+ .map((target) => {
32
+ if (parent.contains(target)) {
33
+ return target;
34
+ }
35
+ const host = unwrapHost(target);
36
+ return host && parent.contains(host) ? host : null;
37
+ })
38
+ .filter((element) => element != null);
39
+ }
40
+ /** The set of nodes on the path from each target up to the root — the "keep" subtree. */
41
+ function buildKeepSet(targets) {
42
+ const keep = new Set();
43
+ targets.forEach((target) => {
44
+ let node = target;
45
+ while (node && !keep.has(node)) {
46
+ keep.add(node);
47
+ node = node.parentNode;
48
+ }
49
+ });
50
+ return keep;
51
+ }
52
+ /** Collects every element outside the kept subtree (a sibling of the kept ancestor chain). */
53
+ function collectOutsideElements(root, keep, stop) {
54
+ const outside = [];
55
+ const walk = (parent) => {
56
+ if (!parent || stop.has(parent)) {
57
+ return;
58
+ }
59
+ Array.from(parent.children).forEach((node) => {
60
+ if (node.nodeName.toLowerCase() === 'script') {
61
+ return;
62
+ }
63
+ if (keep.has(node)) {
64
+ walk(node);
65
+ }
66
+ else {
67
+ outside.push(node);
68
+ }
69
+ });
70
+ };
71
+ walk(root);
72
+ return outside;
73
+ }
74
+ function markOthers(avoidElements, options = {}) {
75
+ const { ariaHidden = false, inert = false, mark = true } = options;
76
+ const first = avoidElements[0];
77
+ if (!first) {
78
+ return () => { };
79
+ }
80
+ const body = first.ownerDocument.body;
81
+ const avoid = correctElements(body, avoidElements);
82
+ // `inert` wins over `aria-hidden` (it already removes the subtree from the a11y tree, Base UI).
83
+ const controlAttribute = inert ? 'inert' : ariaHidden ? 'aria-hidden' : null;
84
+ const controlledElements = [];
85
+ const markedElements = [];
86
+ if (controlAttribute) {
87
+ const counters = controlCounters[controlAttribute];
88
+ const preExisting = preExistingControlled[controlAttribute];
89
+ // `aria-live` regions stay announceable, so keep them out of the controlled set too.
90
+ const live = correctElements(body, Array.from(body.querySelectorAll('[aria-live]')));
91
+ const controlElements = avoid.concat(live);
92
+ const targets = collectOutsideElements(body, buildKeepSet(controlElements), new Set(controlElements));
93
+ targets.forEach((node) => {
94
+ const attr = node.getAttribute(controlAttribute);
95
+ const already = attr !== null && attr !== 'false';
96
+ const count = (counters.get(node) ?? 0) + 1;
97
+ counters.set(node, count);
98
+ controlledElements.push(node);
99
+ if (count === 1 && already) {
100
+ preExisting.add(node);
101
+ }
102
+ if (!already) {
103
+ node.setAttribute(controlAttribute, controlAttribute === 'inert' ? '' : 'true');
104
+ }
105
+ });
106
+ }
107
+ if (mark) {
108
+ const targets = collectOutsideElements(body, buildKeepSet(avoid), new Set(avoid));
109
+ targets.forEach((node) => {
110
+ const count = (markerCounters.get(node) ?? 0) + 1;
111
+ markerCounters.set(node, count);
112
+ markedElements.push(node);
113
+ if (count === 1) {
114
+ node.setAttribute(RDX_FLOATING_MARKER, '');
115
+ }
116
+ });
117
+ }
118
+ lockCount += 1;
119
+ return () => {
120
+ if (controlAttribute) {
121
+ const counters = controlCounters[controlAttribute];
122
+ const preExisting = preExistingControlled[controlAttribute];
123
+ controlledElements.forEach((element) => {
124
+ const count = (counters.get(element) ?? 0) - 1;
125
+ counters.set(element, count);
126
+ if (count === 0) {
127
+ if (!preExisting.has(element)) {
128
+ element.removeAttribute(controlAttribute);
129
+ }
130
+ preExisting.delete(element);
131
+ }
132
+ });
133
+ }
134
+ markedElements.forEach((element) => {
135
+ const count = (markerCounters.get(element) ?? 0) - 1;
136
+ markerCounters.set(element, count);
137
+ if (count === 0) {
138
+ element.removeAttribute(RDX_FLOATING_MARKER);
139
+ }
140
+ });
141
+ lockCount -= 1;
142
+ if (lockCount === 0) {
143
+ // No active locks anywhere — drop the ref-count tables so detached elements can be GC'd.
144
+ controlCounters.inert = new WeakMap();
145
+ controlCounters['aria-hidden'] = new WeakMap();
146
+ preExistingControlled.inert = new WeakSet();
147
+ preExistingControlled['aria-hidden'] = new WeakSet();
148
+ markerCounters = new WeakMap();
149
+ }
150
+ };
151
+ }
152
+
153
+ /** Normalizes a DOM event into Base UI-like interaction intent for focus policy decisions. */
154
+ function getInteractionTypeFromEvent(event) {
155
+ if (!event) {
156
+ return null;
157
+ }
158
+ if (typeof KeyboardEvent !== 'undefined' && event instanceof KeyboardEvent) {
159
+ return 'keyboard';
160
+ }
161
+ if ('pointerType' in event && typeof event.pointerType === 'string') {
162
+ return (event.pointerType || 'mouse');
163
+ }
164
+ if (typeof MouseEvent !== 'undefined' && event instanceof MouseEvent) {
165
+ return event.detail === 0 ? 'keyboard' : 'mouse';
166
+ }
167
+ return '';
168
+ }
169
+ /** Resolves an {@link RdxFocusTarget} (element | getter | null) to a concrete element. */
170
+ function resolveFocusTarget(target) {
171
+ return typeof target === 'function' ? target() : target;
172
+ }
173
+ /**
174
+ * Resolves an {@link RdxInitialFocus} policy against how the popup opened.
175
+ */
176
+ function resolveInitialFocus(policy, openInteractionType) {
177
+ const resolved = typeof policy === 'function' ? policy(openInteractionType) : policy;
178
+ return resolved === false ? false : resolveFocusTarget(resolved);
179
+ }
180
+ /**
181
+ * Resolves an {@link RdxReturnFocus} policy against how the popup closed. `false` = do not return focus;
182
+ * `true` = the default (return to the previously-focused element); an element = return there.
183
+ */
184
+ function resolveReturnFocus(policy, closeInteractionType) {
185
+ const resolved = typeof policy === 'function' ? policy(closeInteractionType) : policy;
186
+ return typeof resolved === 'boolean' ? resolved : resolveFocusTarget(resolved);
187
+ }
188
+ const RDX_FLOATING_FOCUS_MANAGER_CONFIG = new InjectionToken('RdxFloatingFocusManagerConfig');
189
+ /** Provides a {@link RdxFloatingFocusManagerConfig} for an enclosing primitive's focus manager. */
190
+ function provideFloatingFocusManagerConfig(factory) {
191
+ return { provide: RDX_FLOATING_FOCUS_MANAGER_CONFIG, useFactory: factory };
192
+ }
193
+ /** Coerces a boolean-ish input while preserving `undefined` ("not set" → fall back to the config). */
194
+ function coerceOptionalBoolean(value) {
195
+ return value === undefined ? undefined : booleanAttribute(value);
196
+ }
197
+ /**
198
+ * Provides a {@link RdxFocusScopeConfig} whose `trapped` is a **writable** signal, so the enclosing
199
+ * {@link RdxFloatingFocusManager} can drive it from its `modal`/`enabled` policy after construction. The
200
+ * factory has **no** dependency on the manager instance, so it cannot deadlock the host-directive
201
+ * construction order (the manager later injects this same config and writes the signal).
202
+ */
203
+ function provideManagedFocusScopeConfig() {
204
+ return provideRdxFocusScopeConfig(() => ({ trapped: signal(false) }));
205
+ }
206
+ /**
207
+ * `RdxFloatingFocusManager` (ADR 0017 Phase 1b skeleton) — the Angular counterpart of Base UI's
208
+ * `FloatingFocusManager`. It is a **coordinator** that composes three low-level focus parts (it never
209
+ * inherits them, which would re-fuse trap + popup policy): the **reworked {@link RdxFocusScope}** (the
210
+ * trap, via `hostDirectives`), the portal-focus bridge, and owner-`Document` guards. Per ADR 0017 §1/§2
211
+ * its policies are **independent**, none derived from `modal`.
212
+ *
213
+ * **This skeleton wires the composition + lifecycle gates:**
214
+ * - `enabled` — the manager's active-ness (`mounted && !hover-open`). When off, **no trap** (and, later,
215
+ * no aria-hidden / no marker — Phase 2).
216
+ * - `modal` → `RdxFocusScope.trapped`: the effective trap is `enabled() && modal()`, pushed into the
217
+ * composed focus scope through its config token (the composition seam).
218
+ * - `loop` is forwarded to `RdxFocusScope`.
219
+ * - `initialFocus` / `returnFocus` are **orchestrated** here (the §2 policy contract, incl. the
220
+ * interaction-type callback forms): `initialFocus` via the scope's `mountAutoFocus` hook, `returnFocus`
221
+ * via the scope's `returnFocus` config seam (resolved at the scope's queued post-unmount frame).
222
+ */
223
+ class RdxFloatingFocusManager {
224
+ constructor() {
225
+ /** Manager active-ness (ADR 0017 §2): the popup is mounted **and** not hover-opened. */
226
+ this.enabled = input(undefined, { ...(ngDevMode ? { debugName: "enabled" } : /* istanbul ignore next */ {}), transform: coerceOptionalBoolean });
227
+ /** Modal popup → focus trap. Combined with `enabled` to drive the composed `RdxFocusScope`. */
228
+ this.modal = input(undefined, { ...(ngDevMode ? { debugName: "modal" } : /* istanbul ignore next */ {}), transform: coerceOptionalBoolean });
229
+ /**
230
+ * Whether outside elements receive the real `inert` attribute. Defaults to the effective `modal` value,
231
+ * but primitives can split focus trapping from pointer/AT isolation (Base UI `modal="trap-focus"`).
232
+ */
233
+ this.inert = input(undefined, { ...(ngDevMode ? { debugName: "inert" } : /* istanbul ignore next */ {}), transform: coerceOptionalBoolean });
234
+ /** Where focus goes when the popup opens (ADR 0017 §2). */
235
+ this.initialFocus = input(undefined, ...(ngDevMode ? [{ debugName: "initialFocus" }] : /* istanbul ignore next */ []));
236
+ /** Where focus returns when the popup closes (ADR 0017 §2). */
237
+ this.returnFocus = input(undefined, ...(ngDevMode ? [{ debugName: "returnFocus" }] : /* istanbul ignore next */ []));
238
+ /**
239
+ * Restores focus inside the floating tree when the currently focused inside element is removed and
240
+ * the browser drops focus onto `<body>` (Base UI `restoreFocus`). `true` restores to the last
241
+ * available tabbable candidate, `'popup'` restores to the popup container.
242
+ */
243
+ this.restoreFocus = input(undefined, ...(ngDevMode ? [{ debugName: "restoreFocus" }] : /* istanbul ignore next */ []));
244
+ /** Overrides where backward tabbing out of a non-modal portaled popup lands. */
245
+ this.previousFocusableElement = input(undefined, ...(ngDevMode ? [{ debugName: "previousFocusableElement" }] : /* istanbul ignore next */ []));
246
+ /** Overrides where forward tabbing out of a non-modal portaled popup lands. */
247
+ this.nextFocusableElement = input(undefined, ...(ngDevMode ? [{ debugName: "nextFocusableElement" }] : /* istanbul ignore next */ []));
248
+ /** Optional callback/signal that receives the leading content focus guard. */
249
+ this.beforeContentFocusGuardRef = input(undefined, ...(ngDevMode ? [{ debugName: "beforeContentFocusGuardRef" }] : /* istanbul ignore next */ []));
250
+ /** Explicit floating tree for detached sibling composition (Base UI `externalTree`). */
251
+ this.externalTree = input(undefined, ...(ngDevMode ? [{ debugName: "externalTree" }] : /* istanbul ignore next */ []));
252
+ /** Additional elements treated as inside the floating subtree (Base UI `getInsideElements`). */
253
+ this.getInsideElements = input(undefined, ...(ngDevMode ? [{ debugName: "getInsideElements" }] : /* istanbul ignore next */ []));
254
+ /**
255
+ * Whether a **non-modal** popup closes when focus leaves to an unrelated node (Base UI
256
+ * `closeOnFocusOut`, default `true`; Dialog sets it to `!disablePointerDismissal`). Modal popups
257
+ * never close on focus-out (the trap keeps focus in).
258
+ */
259
+ this.closeOnFocusOut = input(undefined, { ...(ngDevMode ? { debugName: "closeOnFocusOut" } : /* istanbul ignore next */ {}), transform: coerceOptionalBoolean });
260
+ /**
261
+ * Emitted when focus leaves a non-modal popup to a node **unrelated** to the floating tree (ADR 0017
262
+ * §3) — the consumer should close the popup. This is the focus-manager's focus-out close (it reads
263
+ * the shared tree), replacing the dismissal capability's focus-out at the ADR 0015 Phase-4 cutover.
264
+ */
265
+ this.focusOut = output();
266
+ /** Optional DI config a composing primitive provides to drive the gates (input wins over config). */
267
+ this.config = inject(RDX_FLOATING_FOCUS_MANAGER_CONFIG, { optional: true });
268
+ /** Effective gates: `input ?? config ?? default`. */
269
+ this.effectiveEnabled = computed(() => this.enabled() ?? this.config?.enabled?.() ?? true, ...(ngDevMode ? [{ debugName: "effectiveEnabled" }] : /* istanbul ignore next */ []));
270
+ this.effectiveModal = computed(() => this.modal() ?? this.config?.modal?.() ?? false, ...(ngDevMode ? [{ debugName: "effectiveModal" }] : /* istanbul ignore next */ []));
271
+ this.effectiveInert = computed(() => this.inert() ?? this.config?.inert?.() ?? this.effectiveModal(), ...(ngDevMode ? [{ debugName: "effectiveInert" }] : /* istanbul ignore next */ []));
272
+ this.effectiveCloseOnFocusOut = computed(() => this.closeOnFocusOut() ?? this.config?.closeOnFocusOut?.() ?? true, ...(ngDevMode ? [{ debugName: "effectiveCloseOnFocusOut" }] : /* istanbul ignore next */ []));
273
+ this.effectiveInitialFocus = computed(() => this.initialFocus() !== undefined ? this.initialFocus() : (this.config?.initialFocus?.() ?? null), ...(ngDevMode ? [{ debugName: "effectiveInitialFocus" }] : /* istanbul ignore next */ []));
274
+ this.effectiveReturnFocus = computed(() => this.returnFocus() !== undefined ? this.returnFocus() : (this.config?.returnFocus?.() ?? true), ...(ngDevMode ? [{ debugName: "effectiveReturnFocus" }] : /* istanbul ignore next */ []));
275
+ this.effectiveRestoreFocus = computed(() => this.restoreFocus() ?? this.config?.restoreFocus?.() ?? false, ...(ngDevMode ? [{ debugName: "effectiveRestoreFocus" }] : /* istanbul ignore next */ []));
276
+ this.effectivePreviousFocusableElement = computed(() => this.previousFocusableElement() !== undefined
277
+ ? this.previousFocusableElement()
278
+ : this.config?.previousFocusableElement?.(), ...(ngDevMode ? [{ debugName: "effectivePreviousFocusableElement" }] : /* istanbul ignore next */ []));
279
+ this.effectiveNextFocusableElement = computed(() => this.nextFocusableElement() !== undefined ? this.nextFocusableElement() : this.config?.nextFocusableElement?.(), ...(ngDevMode ? [{ debugName: "effectiveNextFocusableElement" }] : /* istanbul ignore next */ []));
280
+ this.effectiveBeforeContentFocusGuardRef = computed(() => this.beforeContentFocusGuardRef() ?? this.config?.beforeContentFocusGuardRef?.(), ...(ngDevMode ? [{ debugName: "effectiveBeforeContentFocusGuardRef" }] : /* istanbul ignore next */ []));
281
+ this.effectiveExternalTree = computed(() => this.externalTree() ?? this.config?.externalTree?.() ?? null, ...(ngDevMode ? [{ debugName: "effectiveExternalTree" }] : /* istanbul ignore next */ []));
282
+ this.insideElements = computed(() => {
283
+ const getElements = this.getInsideElements() ?? this.config?.getInsideElements;
284
+ return (getElements?.() ?? []).filter((element) => element != null);
285
+ }, ...(ngDevMode ? [{ debugName: "insideElements" }] : /* istanbul ignore next */ []));
286
+ this.effectiveOpenInteractionType = computed(() => this.config?.openInteractionType?.() ?? this._interactionType(), ...(ngDevMode ? [{ debugName: "effectiveOpenInteractionType" }] : /* istanbul ignore next */ []));
287
+ this.effectiveCloseInteractionType = computed(() => this.config?.closeInteractionType?.() ?? this._interactionType(), ...(ngDevMode ? [{ debugName: "effectiveCloseInteractionType" }] : /* istanbul ignore next */ []));
288
+ /** The effective trap state the composed `RdxFocusScope` reads via its config token. */
289
+ this.trapped = computed(() => this.effectiveEnabled() && this.effectiveModal(), ...(ngDevMode ? [{ debugName: "trapped" }] : /* istanbul ignore next */ []));
290
+ // The config this directive provides — its `trapped` signal is writable so we can drive it, and we
291
+ // attach a `returnFocus` resolver the composed focus scope calls at unmount (ADR 0017 `returnFocus`).
292
+ this.focusScopeConfig = inject(RdxFocusScopeConfigToken);
293
+ this.host = inject(ElementRef).nativeElement;
294
+ this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
295
+ /** The shared per-popup context (open / triggers / elements), if a primitive root provides one. */
296
+ this.rootContext = inject(RDX_FLOATING_ROOT_CONTEXT, { optional: true });
297
+ /** The registration handle for this node, used to read the shared tree (ancestors / descendants). */
298
+ this.registration = inject(RDX_FLOATING_REGISTRATION, { optional: true });
299
+ this._interactionType = signal('', ...(ngDevMode ? [{ debugName: "_interactionType" }] : /* istanbul ignore next */ []));
300
+ /** How the popup was most recently interacted with — fed to the initial/return focus policy callbacks. */
301
+ this.interactionType = this._interactionType.asReadonly();
302
+ effect(() => this.focusScopeConfig.trapped.set(this.trapped()));
303
+ // Own the return-focus *target* (the composed focus scope owns the *timing*): the scope calls this
304
+ // in its queued post-unmount frame, resolving the `returnFocus` policy against the close interaction.
305
+ this.focusScopeConfig.returnFocus = () => this.resolveReturnFocusTarget();
306
+ if (!this.isBrowser) {
307
+ return; // SSR: no DOM marking / listeners.
308
+ }
309
+ // Marker pass (ADR 0017 §3) — applied to outside elements while the manager is active **and the
310
+ // popup is open**, independent of `modal`. Read by ADR 0015's outside-press guard.
311
+ effect((onCleanup) => {
312
+ if (!this.effectiveEnabled() || !this.isFloatingOpen()) {
313
+ return;
314
+ }
315
+ onCleanup(markOthers(this.avoidElements(), { ariaHidden: false, mark: true }));
316
+ });
317
+ // Pointer/AT isolation pass — apply the real `inert` attribute to outside elements only when the
318
+ // composing primitive asks for outside interaction to be blocked. This is intentionally separate from
319
+ // focus trapping: Base UI `modal="trap-focus"` traps focus but leaves outside pointer interaction
320
+ // enabled.
321
+ effect((onCleanup) => {
322
+ if (!this.effectiveEnabled() || !this.isFloatingOpen() || !this.effectiveInert()) {
323
+ return;
324
+ }
325
+ onCleanup(markOthers(this.avoidElements(), { inert: true, mark: false }));
326
+ });
327
+ this.trackInteractionType();
328
+ useFocusGuardsTabbability(() => (this.rootContext?.floatingElement ?? this.host), {
329
+ enabled: () => Boolean(this.rootContext) && this.effectiveEnabled() && this.isFloatingOpen() && !this.effectiveModal()
330
+ });
331
+ this.wireCloseOnFocusOut();
332
+ this.wireRestoreFocus();
333
+ this.wirePortalFocusBridge();
334
+ this.wireFocusOrchestration();
335
+ this.wireInitialFocusFallback();
336
+ }
337
+ /** Records the most recent open/close interaction (pointer type or keyboard) for the focus policies. */
338
+ trackInteractionType() {
339
+ const ownerDocument = this.host.ownerDocument;
340
+ const onPointer = (event) => {
341
+ this._interactionType.set((event.pointerType || 'mouse'));
342
+ };
343
+ const onKey = () => this._interactionType.set('keyboard');
344
+ ownerDocument.addEventListener('pointerdown', onPointer, true);
345
+ ownerDocument.addEventListener('keydown', onKey, true);
346
+ inject(DestroyRef).onDestroy(() => {
347
+ ownerDocument.removeEventListener('pointerdown', onPointer, true);
348
+ ownerDocument.removeEventListener('keydown', onKey, true);
349
+ });
350
+ }
351
+ /**
352
+ * Initial-focus orchestration (ADR 0017 §2). The manager owns the focus *policy*; it intercepts the
353
+ * composed {@link RdxFocusScope}'s preventable `mountAutoFocus` (its designed extension point) and
354
+ * applies the `initialFocus` policy, falling back to the scope's first-tabbable default when the
355
+ * policy is `null`. (`returnFocus` is orchestrated separately via the config seam — see
356
+ * {@link resolveReturnFocusTarget} — because it must run during the scope's queued *post-unmount* frame.)
357
+ */
358
+ wireFocusOrchestration() {
359
+ const focusScope = inject(RdxFocusScope);
360
+ focusScope.mountAutoFocus.subscribe((event) => {
361
+ const interactionType = this.effectiveOpenInteractionType();
362
+ const target = resolveInitialFocus(this.effectiveInitialFocus(), interactionType) ??
363
+ this.defaultInitialFocus(interactionType);
364
+ if (target === false) {
365
+ event.preventDefault();
366
+ return;
367
+ }
368
+ if (target) {
369
+ event.preventDefault(); // override the scope's first-tabbable default
370
+ target.focus();
371
+ }
372
+ });
373
+ }
374
+ /**
375
+ * Resolves the {@link returnFocus} policy against the **close** interaction type for the composed
376
+ * focus scope to apply at unmount (ADR 0017 §2). Mirrors Base UI's `getReturnElement`:
377
+ * - `false` → `false` (the scope suppresses return-focus);
378
+ * - `true` / `null` → `undefined` (the scope's default — return to the element focused before mount);
379
+ * - an element (direct or from a callback) → that element (returned **explicitly**, bypassing the
380
+ * "focus moved elsewhere" guard).
381
+ */
382
+ resolveReturnFocusTarget() {
383
+ const resolved = resolveReturnFocus(this.effectiveReturnFocus(), this.effectiveCloseInteractionType());
384
+ if (resolved === false) {
385
+ return false;
386
+ }
387
+ if (resolved === undefined) {
388
+ return false;
389
+ }
390
+ if (resolved === true || resolved == null) {
391
+ return undefined;
392
+ }
393
+ return resolved;
394
+ }
395
+ /**
396
+ * Base UI's `defaultInitialFocus`: on a **touch** open, focus the popup itself instead of its first
397
+ * tabbable control, so a soft keyboard (Android) does not pop up over the popup. Any other interaction
398
+ * returns `null`, keeping the focus scope's first-tabbable default. The popup is made programmatically
399
+ * focusable (`tabindex="-1"`) if it isn't already.
400
+ */
401
+ defaultInitialFocus(interactionType) {
402
+ if (interactionType !== 'touch') {
403
+ return null;
404
+ }
405
+ const popup = (this.rootContext?.floatingElement ?? this.host);
406
+ if (!popup.hasAttribute('tabindex')) {
407
+ popup.setAttribute('tabindex', '-1');
408
+ }
409
+ return popup;
410
+ }
411
+ /**
412
+ * Manager-owned post-open fallback. Some popup types open from a trigger event that fired before the
413
+ * manager existed, and some policies resolve their final target only after a couple of renders. Re-run
414
+ * the initial-focus policy for a few animation frames while the popup is open so the target can settle.
415
+ */
416
+ wireInitialFocusFallback() {
417
+ effect(() => {
418
+ if (!this.effectiveEnabled() || !this.isFloatingOpen()) {
419
+ return;
420
+ }
421
+ this.scheduleInitialFocusFallback();
422
+ });
423
+ }
424
+ scheduleInitialFocusFallback(attempt = 0) {
425
+ const view = this.host.ownerDocument.defaultView ?? globalThis;
426
+ view.requestAnimationFrame(() => this.applyInitialFocusFallback(attempt));
427
+ }
428
+ applyInitialFocusFallback(attempt) {
429
+ if (!this.effectiveEnabled() || !this.isFloatingOpen()) {
430
+ return;
431
+ }
432
+ const popup = (this.rootContext?.floatingElement ?? this.host);
433
+ const activeElement = this.host.ownerDocument.activeElement;
434
+ if (activeElement instanceof HTMLElement && popup.contains(activeElement)) {
435
+ return;
436
+ }
437
+ const interactionType = this.effectiveOpenInteractionType();
438
+ const resolved = resolveInitialFocus(this.effectiveInitialFocus(), interactionType);
439
+ if (resolved === false) {
440
+ return;
441
+ }
442
+ const target = resolved ?? this.defaultInitialFocus(interactionType);
443
+ if (target && activeElement !== target) {
444
+ target.focus({ preventScroll: true });
445
+ }
446
+ if (attempt < 2) {
447
+ this.scheduleInitialFocusFallback(attempt + 1);
448
+ }
449
+ }
450
+ /**
451
+ * Close-on-focus-out (ADR 0017 §3): a **non-modal** active popup closes when focus moves to a node
452
+ * unrelated to the floating tree — not the popup, its trigger(s), a focus guard, or an ancestor /
453
+ * descendant popup — and not during a pointer press (a drag must not close it). Mirrors Base UI's
454
+ * `FloatingFocusManager` `!modal` branch (`movedToUnrelatedNode`).
455
+ */
456
+ wireCloseOnFocusOut() {
457
+ const ownerDocument = this.host.ownerDocument;
458
+ let pointerDown = false;
459
+ const onPointerDown = () => {
460
+ pointerDown = true;
461
+ };
462
+ const onPointerUp = () => {
463
+ pointerDown = false;
464
+ };
465
+ const onFocusOut = (event) => {
466
+ if (!this.effectiveEnabled() || !this.effectiveCloseOnFocusOut() || this.effectiveModal() || pointerDown) {
467
+ return;
468
+ }
469
+ const relatedTarget = event.relatedTarget;
470
+ if (!relatedTarget) {
471
+ return; // focus left to nothing (tab-away / window blur) — let the browser handle it
472
+ }
473
+ if (relatedTarget instanceof Element && relatedTarget.hasAttribute(FOCUS_GUARD_ATTR)) {
474
+ return; // moved onto a focus guard — still inside the focus system
475
+ }
476
+ if (this.isRelatedTargetInside(relatedTarget)) {
477
+ return; // moved to a related node (trigger / ancestor / descendant) — keep open
478
+ }
479
+ this.focusOut.emit(event);
480
+ };
481
+ ownerDocument.addEventListener('pointerdown', onPointerDown, true);
482
+ ownerDocument.addEventListener('pointerup', onPointerUp, true);
483
+ ownerDocument.addEventListener('focusout', onFocusOut, true);
484
+ inject(DestroyRef).onDestroy(() => {
485
+ ownerDocument.removeEventListener('pointerdown', onPointerDown, true);
486
+ ownerDocument.removeEventListener('pointerup', onPointerUp, true);
487
+ ownerDocument.removeEventListener('focusout', onFocusOut, true);
488
+ });
489
+ }
490
+ /**
491
+ * Restore focus when an inside focused element disappears and the document focus falls back to
492
+ * `<body>`. This mirrors the practical Base UI `restoreFocus` path used by Select while keeping the
493
+ * broader portal-guard navigation separate.
494
+ */
495
+ wireRestoreFocus() {
496
+ const ownerDocument = this.host.ownerDocument;
497
+ const onFocusOut = (event) => {
498
+ if (!this.effectiveEnabled() || !this.effectiveRestoreFocus() || !this.isFloatingOpen()) {
499
+ return;
500
+ }
501
+ const target = event.target;
502
+ if (!target || !this.isRelatedTargetInside(target)) {
503
+ return;
504
+ }
505
+ const view = ownerDocument.defaultView ?? globalThis;
506
+ view.requestAnimationFrame(() => {
507
+ if (!this.effectiveEnabled() || !this.effectiveRestoreFocus() || !this.isFloatingOpen()) {
508
+ return;
509
+ }
510
+ if (ownerDocument.activeElement !== ownerDocument.body) {
511
+ return;
512
+ }
513
+ const popup = this.rootContext?.floatingElement ?? this.host;
514
+ const targetToFocus = this.effectiveRestoreFocus() === 'popup' ? popup : (getTabbableCandidates(popup).at(-1) ?? popup);
515
+ focus(targetToFocus);
516
+ });
517
+ };
518
+ ownerDocument.addEventListener('focusout', onFocusOut, true);
519
+ inject(DestroyRef).onDestroy(() => ownerDocument.removeEventListener('focusout', onFocusOut, true));
520
+ }
521
+ /**
522
+ * Portal focus bridge (Base UI inside guards): when focus reaches the hidden guards around portaled
523
+ * content, redirect it either back inside the popup or to the configured neighboring tabbable node.
524
+ */
525
+ wirePortalFocusBridge() {
526
+ effect((onCleanup) => {
527
+ if (!this.effectiveEnabled() || !this.isFloatingOpen()) {
528
+ this.setBeforeContentFocusGuardRef(null);
529
+ return;
530
+ }
531
+ const ownerDocument = this.host.ownerDocument;
532
+ const parent = this.host.parentNode;
533
+ if (!parent) {
534
+ return;
535
+ }
536
+ const beforeGuard = createFocusGuard(ownerDocument);
537
+ const afterGuard = createFocusGuard(ownerDocument);
538
+ beforeGuard.setAttribute('data-type', 'inside');
539
+ afterGuard.setAttribute('data-type', 'inside');
540
+ parent.insertBefore(beforeGuard, this.host);
541
+ parent.insertBefore(afterGuard, this.host.nextSibling);
542
+ this.setBeforeContentFocusGuardRef(beforeGuard);
543
+ const onBeforeFocus = (event) => {
544
+ if (this.effectiveModal()) {
545
+ focus(this.getTabbableContent().at(-1) ?? this.host);
546
+ return;
547
+ }
548
+ if (this.isFromOutsideFocusGuard(event)) {
549
+ const popup = this.rootContext?.floatingElement ?? this.host;
550
+ enableFocusInside(popup);
551
+ focus(this.getTabbableContent()[0] ?? this.host);
552
+ return;
553
+ }
554
+ if (isOutsideEvent(event, this.portalFocusContainer())) {
555
+ focus(getNextTabbable(this.rootContext?.referenceElement ?? this.host));
556
+ return;
557
+ }
558
+ focus(resolveFocusTarget(this.effectivePreviousFocusableElement()) ?? this.defaultPreviousFocusable());
559
+ };
560
+ const onAfterFocus = (event) => {
561
+ if (this.effectiveModal()) {
562
+ focus(this.getTabbableContent()[0] ?? this.host);
563
+ return;
564
+ }
565
+ if (isOutsideEvent(event, this.portalFocusContainer())) {
566
+ focus(getPreviousTabbable(this.rootContext?.referenceElement ?? this.host));
567
+ return;
568
+ }
569
+ focus(resolveFocusTarget(this.effectiveNextFocusableElement()) ?? this.defaultNextFocusable());
570
+ };
571
+ beforeGuard.addEventListener('focus', onBeforeFocus);
572
+ afterGuard.addEventListener('focus', onAfterFocus);
573
+ onCleanup(() => {
574
+ beforeGuard.removeEventListener('focus', onBeforeFocus);
575
+ afterGuard.removeEventListener('focus', onAfterFocus);
576
+ beforeGuard.remove();
577
+ afterGuard.remove();
578
+ this.setBeforeContentFocusGuardRef(null);
579
+ });
580
+ });
581
+ }
582
+ /**
583
+ * The marker keep-set is intentionally narrow: the popup/focus host only. Own sibling roots such as a
584
+ * user backdrop are DOM-footprint bookkeeping, not marker keep-set members.
585
+ */
586
+ avoidElements() {
587
+ return [
588
+ this.host,
589
+ ...this.insideElements(),
590
+ resolveFocusTarget(this.effectivePreviousFocusableElement()) ?? null,
591
+ resolveFocusTarget(this.effectiveNextFocusableElement()) ?? null
592
+ ].filter((element) => element != null);
593
+ }
594
+ isFloatingOpen() {
595
+ return this.rootContext?.open() ?? true;
596
+ }
597
+ /** Whether `relatedTarget` is inside the popup, its trigger(s), or an ancestor / descendant popup. */
598
+ isRelatedTargetInside(relatedTarget) {
599
+ const floating = this.rootContext?.floatingElement ?? this.host;
600
+ if (composedContains(floating, relatedTarget)) {
601
+ return true;
602
+ }
603
+ if (this.rootContext && this.contextContains(this.rootContext, relatedTarget)) {
604
+ return true;
605
+ }
606
+ if (this.insideElements().some((element) => element === relatedTarget || composedContains(element, relatedTarget))) {
607
+ return true;
608
+ }
609
+ const node = this.currentFloatingNode();
610
+ if (node) {
611
+ for (const ancestor of node.tree.ancestors(node)) {
612
+ if (ancestor.context && this.contextContains(ancestor.context, relatedTarget)) {
613
+ return true;
614
+ }
615
+ }
616
+ for (const child of node.tree.children(node, { onlyOpen: true })) {
617
+ if (child.context && this.contextContains(child.context, relatedTarget)) {
618
+ return true;
619
+ }
620
+ }
621
+ }
622
+ return false;
623
+ }
624
+ contextContains(context, relatedTarget) {
625
+ if (context.floatingElement && composedContains(context.floatingElement, relatedTarget)) {
626
+ return true;
627
+ }
628
+ if (context.referenceElement && composedContains(context.referenceElement, relatedTarget)) {
629
+ return true;
630
+ }
631
+ return context.triggers.contains(relatedTarget);
632
+ }
633
+ currentFloatingNode() {
634
+ const node = this.registration?.node() ?? null;
635
+ if (node) {
636
+ return node;
637
+ }
638
+ const tree = this.effectiveExternalTree();
639
+ return tree?.all.find((candidate) => candidate.context === this.rootContext) ?? null;
640
+ }
641
+ getTabbableContent() {
642
+ return getTabbableCandidates(this.rootContext?.floatingElement ?? this.host);
643
+ }
644
+ portalFocusContainer() {
645
+ const floating = this.rootContext?.floatingElement ?? this.host;
646
+ const parent = floating.parentElement;
647
+ return parent && parent !== floating.ownerDocument.body ? parent : floating;
648
+ }
649
+ defaultPreviousFocusable() {
650
+ return getPreviousTabbable(this.rootContext?.referenceElement ?? this.host);
651
+ }
652
+ defaultNextFocusable() {
653
+ return getNextTabbable(this.rootContext?.referenceElement ?? this.host);
654
+ }
655
+ setBeforeContentFocusGuardRef(element) {
656
+ const ref = this.effectiveBeforeContentFocusGuardRef();
657
+ if (!ref) {
658
+ return;
659
+ }
660
+ if (typeof ref === 'function') {
661
+ ref(element);
662
+ }
663
+ else {
664
+ ref.set(element);
665
+ }
666
+ }
667
+ isFromOutsideFocusGuard(event) {
668
+ const relatedTarget = event.relatedTarget;
669
+ return (relatedTarget instanceof Element &&
670
+ relatedTarget.hasAttribute(FOCUS_GUARD_ATTR) &&
671
+ relatedTarget.getAttribute('data-type') === 'outside');
672
+ }
673
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxFloatingFocusManager, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
674
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxFloatingFocusManager, isStandalone: true, selector: "[rdxFloatingFocusManager]", inputs: { enabled: { classPropertyName: "enabled", publicName: "enabled", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, inert: { classPropertyName: "inert", publicName: "inert", isSignal: true, isRequired: false, transformFunction: null }, initialFocus: { classPropertyName: "initialFocus", publicName: "initialFocus", isSignal: true, isRequired: false, transformFunction: null }, returnFocus: { classPropertyName: "returnFocus", publicName: "returnFocus", isSignal: true, isRequired: false, transformFunction: null }, restoreFocus: { classPropertyName: "restoreFocus", publicName: "restoreFocus", isSignal: true, isRequired: false, transformFunction: null }, previousFocusableElement: { classPropertyName: "previousFocusableElement", publicName: "previousFocusableElement", isSignal: true, isRequired: false, transformFunction: null }, nextFocusableElement: { classPropertyName: "nextFocusableElement", publicName: "nextFocusableElement", isSignal: true, isRequired: false, transformFunction: null }, beforeContentFocusGuardRef: { classPropertyName: "beforeContentFocusGuardRef", publicName: "beforeContentFocusGuardRef", isSignal: true, isRequired: false, transformFunction: null }, externalTree: { classPropertyName: "externalTree", publicName: "externalTree", isSignal: true, isRequired: false, transformFunction: null }, getInsideElements: { classPropertyName: "getInsideElements", publicName: "getInsideElements", isSignal: true, isRequired: false, transformFunction: null }, closeOnFocusOut: { classPropertyName: "closeOnFocusOut", publicName: "closeOnFocusOut", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { focusOut: "focusOut" }, providers: [provideManagedFocusScopeConfig()], exportAs: ["rdxFloatingFocusManager"], hostDirectives: [{ directive: i1.RdxFocusScope, inputs: ["loop", "loop"] }], ngImport: i0 }); }
675
+ }
676
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxFloatingFocusManager, decorators: [{
677
+ type: Directive,
678
+ args: [{
679
+ selector: '[rdxFloatingFocusManager]',
680
+ exportAs: 'rdxFloatingFocusManager',
681
+ hostDirectives: [{ directive: RdxFocusScope, inputs: ['loop'] }],
682
+ providers: [provideManagedFocusScopeConfig()]
683
+ }]
684
+ }], ctorParameters: () => [], propDecorators: { enabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "enabled", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], inert: [{ type: i0.Input, args: [{ isSignal: true, alias: "inert", required: false }] }], initialFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialFocus", required: false }] }], returnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "returnFocus", required: false }] }], restoreFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "restoreFocus", required: false }] }], previousFocusableElement: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousFocusableElement", required: false }] }], nextFocusableElement: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextFocusableElement", required: false }] }], beforeContentFocusGuardRef: [{ type: i0.Input, args: [{ isSignal: true, alias: "beforeContentFocusGuardRef", required: false }] }], externalTree: [{ type: i0.Input, args: [{ isSignal: true, alias: "externalTree", required: false }] }], getInsideElements: [{ type: i0.Input, args: [{ isSignal: true, alias: "getInsideElements", required: false }] }], closeOnFocusOut: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnFocusOut", required: false }] }], focusOut: [{ type: i0.Output, args: ["focusOut"] }] } });
685
+
686
+ /**
687
+ * Shared trigger state for floating primitives. It intentionally stays small: each primitive still owns
688
+ * its open/close business rules, while this layer normalizes active-trigger state and the open method
689
+ * signal that must be captured before the popup/focus manager mounts.
690
+ */
691
+ function createRdxTriggerInteraction(options) {
692
+ const lastPointerType = signal('', ...(ngDevMode ? [{ debugName: "lastPointerType" }] : /* istanbul ignore next */ []));
693
+ const activeTrigger = options.activeTrigger ?? (() => options.trigger());
694
+ const disabled = computed(() => options.disabled?.() ?? false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
695
+ const isActive = computed(() => options.open() && activeTrigger() === options.trigger(), ...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
696
+ const dataState = computed(() => (options.open() ? 'open' : 'closed'), ...(ngDevMode ? [{ debugName: "dataState" }] : /* istanbul ignore next */ []));
697
+ const dataPopupOpen = computed(() => (isActive() ? '' : undefined), ...(ngDevMode ? [{ debugName: "dataPopupOpen" }] : /* istanbul ignore next */ []));
698
+ const ariaControls = computed(() => (isActive() ? (options.contentId?.() ?? undefined) : undefined), ...(ngDevMode ? [{ debugName: "ariaControls" }] : /* istanbul ignore next */ []));
699
+ const ariaExpanded = computed(() => isActive(), ...(ngDevMode ? [{ debugName: "ariaExpanded" }] : /* istanbul ignore next */ []));
700
+ return {
701
+ lastPointerType: lastPointerType.asReadonly(),
702
+ isActive,
703
+ isInactive: computed(() => !isActive()),
704
+ dataState,
705
+ dataPopupOpen,
706
+ ariaControls,
707
+ ariaExpanded,
708
+ disabled,
709
+ recordPointerDown: (event) => {
710
+ lastPointerType.set(event.pointerType);
711
+ },
712
+ clickInteractionType: (event) => {
713
+ if (event.detail !== 0 && lastPointerType() === 'touch') {
714
+ return 'touch';
715
+ }
716
+ return getInteractionTypeFromEvent(event);
717
+ }
718
+ };
719
+ }
720
+ /**
721
+ * Adds Base UI's trigger-side focus guards for portaled content. The content guards remain owned by
722
+ * `RdxFloatingFocusManager`; these guards bridge tab order from the trigger into the portal and close the
723
+ * popup when focus leaves past either trigger-side boundary.
724
+ */
725
+ function useTriggerFocusGuards(options) {
726
+ const enabled = options.enabled ?? (() => true);
727
+ effect((onCleanup) => {
728
+ if (!enabled()) {
729
+ return;
730
+ }
731
+ const trigger = options.trigger();
732
+ const preGuard = createFocusGuard(trigger.ownerDocument);
733
+ const postGuard = createFocusGuard(trigger.ownerDocument);
734
+ preGuard.setAttribute('data-type', 'outside');
735
+ postGuard.setAttribute('data-type', 'outside');
736
+ const contentId = options.contentId?.();
737
+ const anchor = contentId ? createAriaOwnsAnchor(trigger.ownerDocument, contentId) : null;
738
+ const handlePreGuardFocus = (event) => {
739
+ const previousTabbable = getTabbableBeforeElement(preGuard);
740
+ options.close(event);
741
+ focus(previousTabbable);
742
+ };
743
+ const handlePostGuardFocus = (event) => {
744
+ const popup = options.popupElement?.() ?? null;
745
+ const beforeContentGuard = options.beforeContentFocusGuard?.() ?? null;
746
+ if (popup && beforeContentGuard && isOutsideEvent(event, popup)) {
747
+ focus(beforeContentGuard);
748
+ return;
749
+ }
750
+ let nextTabbable = getTabbableAfterElement(postGuard);
751
+ while (nextTabbable &&
752
+ (nextTabbable.hasAttribute(FOCUS_GUARD_ATTR) || (popup && composedContains(popup, nextTabbable)))) {
753
+ const previous = nextTabbable;
754
+ nextTabbable = getTabbableAfterElement(nextTabbable);
755
+ if (nextTabbable === previous) {
756
+ break;
757
+ }
758
+ }
759
+ options.close(event);
760
+ focus(nextTabbable);
761
+ };
762
+ preGuard.addEventListener('focus', handlePreGuardFocus);
763
+ postGuard.addEventListener('focus', handlePostGuardFocus);
764
+ trigger.insertAdjacentElement('beforebegin', preGuard);
765
+ if (anchor) {
766
+ trigger.insertAdjacentElement('afterend', anchor);
767
+ anchor.insertAdjacentElement('afterend', postGuard);
768
+ }
769
+ else {
770
+ trigger.insertAdjacentElement('afterend', postGuard);
771
+ }
772
+ onCleanup(() => {
773
+ preGuard.removeEventListener('focus', handlePreGuardFocus);
774
+ postGuard.removeEventListener('focus', handlePostGuardFocus);
775
+ preGuard.remove();
776
+ postGuard.remove();
777
+ anchor?.remove();
778
+ });
779
+ });
780
+ }
781
+ /**
782
+ * Backwards-compatible aria-owns-only bridge for modal popups that do not need trigger-side tab guards.
783
+ */
784
+ function useTriggerFocusGuardAnchor(options) {
785
+ const enabled = options.enabled ?? (() => true);
786
+ effect((onCleanup) => {
787
+ const contentId = options.contentId();
788
+ if (!enabled() || !contentId) {
789
+ return;
790
+ }
791
+ const trigger = options.trigger();
792
+ const anchor = createAriaOwnsAnchor(trigger.ownerDocument, contentId);
793
+ trigger.insertAdjacentElement('afterend', anchor);
794
+ onCleanup(() => anchor.remove());
795
+ });
796
+ }
797
+
798
+ /**
799
+ * Generated bundle index. Do not edit.
800
+ */
801
+
802
+ export { RDX_FLOATING_FOCUS_MANAGER_CONFIG, RDX_FLOATING_MARKER, RdxFloatingFocusManager, createRdxTriggerInteraction, getInteractionTypeFromEvent, provideFloatingFocusManagerConfig, resolveFocusTarget, resolveInitialFocus, resolveReturnFocus, useTriggerFocusGuardAnchor, useTriggerFocusGuards };
803
+ //# sourceMappingURL=radix-ng-primitives-floating-focus-manager.mjs.map