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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +76 -6
  3. package/fesm2022/radix-ng-primitives-accordion.mjs +5 -3
  4. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  5. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +31 -24
  6. package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
  7. package/fesm2022/radix-ng-primitives-autocomplete.mjs +1744 -0
  8. package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -0
  9. package/fesm2022/radix-ng-primitives-calendar.mjs +5 -3
  10. package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
  11. package/fesm2022/radix-ng-primitives-combobox.mjs +1399 -606
  12. package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
  13. package/fesm2022/radix-ng-primitives-config.mjs +13 -4
  14. package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
  15. package/fesm2022/radix-ng-primitives-context-menu.mjs +51 -10
  16. package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
  17. package/fesm2022/radix-ng-primitives-core.mjs +1345 -64
  18. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  19. package/fesm2022/radix-ng-primitives-date-field.mjs +5 -3
  20. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  21. package/fesm2022/radix-ng-primitives-dialog.mjs +271 -145
  22. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  23. package/fesm2022/radix-ng-primitives-direction-provider.mjs +70 -0
  24. package/fesm2022/radix-ng-primitives-direction-provider.mjs.map +1 -0
  25. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +519 -184
  26. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
  27. package/fesm2022/radix-ng-primitives-drawer.mjs +154 -64
  28. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
  29. package/fesm2022/radix-ng-primitives-field.mjs +3 -2
  30. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
  31. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +517 -0
  32. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -0
  33. package/fesm2022/radix-ng-primitives-focus-scope.mjs +296 -70
  34. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  35. package/fesm2022/radix-ng-primitives-menu.mjs +894 -299
  36. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  37. package/fesm2022/radix-ng-primitives-menubar.mjs +32 -4
  38. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  39. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +176 -207
  40. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  41. package/fesm2022/radix-ng-primitives-popover.mjs +250 -250
  42. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  43. package/fesm2022/radix-ng-primitives-popper.mjs +94 -45
  44. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  45. package/fesm2022/radix-ng-primitives-portal.mjs +107 -17
  46. package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
  47. package/fesm2022/radix-ng-primitives-presence.mjs +262 -79
  48. package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
  49. package/fesm2022/radix-ng-primitives-preview-card.mjs +172 -218
  50. package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -1
  51. package/fesm2022/radix-ng-primitives-roving-focus.mjs +4 -2
  52. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  53. package/fesm2022/radix-ng-primitives-scroll-area.mjs +5 -4
  54. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  55. package/fesm2022/radix-ng-primitives-select.mjs +303 -234
  56. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  57. package/fesm2022/radix-ng-primitives-slider.mjs +5 -3
  58. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  59. package/fesm2022/radix-ng-primitives-stepper.mjs +5 -3
  60. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  61. package/fesm2022/radix-ng-primitives-time-field.mjs +5 -3
  62. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  63. package/fesm2022/radix-ng-primitives-toast.mjs +15 -36
  64. package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -1
  65. package/fesm2022/radix-ng-primitives-toggle-group.mjs +5 -3
  66. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  67. package/fesm2022/radix-ng-primitives-toolbar.mjs +5 -3
  68. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  69. package/fesm2022/radix-ng-primitives-tooltip.mjs +105 -145
  70. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  71. package/package.json +14 -1
  72. package/types/radix-ng-primitives-accordion.d.ts +4 -3
  73. package/types/radix-ng-primitives-alert-dialog.d.ts +17 -11
  74. package/types/radix-ng-primitives-autocomplete.d.ts +661 -0
  75. package/types/radix-ng-primitives-calendar.d.ts +5 -3
  76. package/types/radix-ng-primitives-combobox.d.ts +727 -293
  77. package/types/radix-ng-primitives-config.d.ts +1 -1
  78. package/types/radix-ng-primitives-context-menu.d.ts +15 -5
  79. package/types/radix-ng-primitives-core.d.ts +762 -14
  80. package/types/radix-ng-primitives-date-field.d.ts +3 -2
  81. package/types/radix-ng-primitives-dialog.d.ts +107 -55
  82. package/types/radix-ng-primitives-direction-provider.d.ts +41 -0
  83. package/types/radix-ng-primitives-dismissable-layer.d.ts +147 -99
  84. package/types/radix-ng-primitives-drawer.d.ts +49 -22
  85. package/types/radix-ng-primitives-field.d.ts +1 -0
  86. package/types/radix-ng-primitives-floating-focus-manager.d.ts +175 -0
  87. package/types/radix-ng-primitives-focus-scope.d.ts +132 -1
  88. package/types/radix-ng-primitives-menu.d.ts +204 -112
  89. package/types/radix-ng-primitives-navigation-menu.d.ts +61 -101
  90. package/types/radix-ng-primitives-popover.d.ts +82 -115
  91. package/types/radix-ng-primitives-popper.d.ts +46 -10
  92. package/types/radix-ng-primitives-portal.d.ts +53 -8
  93. package/types/radix-ng-primitives-presence.d.ts +98 -17
  94. package/types/radix-ng-primitives-preview-card.d.ts +63 -95
  95. package/types/radix-ng-primitives-roving-focus.d.ts +7 -6
  96. package/types/radix-ng-primitives-scroll-area.d.ts +2 -2
  97. package/types/radix-ng-primitives-select.d.ts +192 -158
  98. package/types/radix-ng-primitives-slider.d.ts +5 -4
  99. package/types/radix-ng-primitives-stepper.d.ts +4 -3
  100. package/types/radix-ng-primitives-time-field.d.ts +3 -2
  101. package/types/radix-ng-primitives-toast.d.ts +7 -7
  102. package/types/radix-ng-primitives-toggle-group.d.ts +5 -4
  103. package/types/radix-ng-primitives-toolbar.d.ts +3 -2
  104. package/types/radix-ng-primitives-tooltip.d.ts +48 -84
@@ -1,59 +1,31 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, signal, inject, Injector, DestroyRef, ElementRef, input, booleanAttribute, computed, output, afterNextRender, effect, Directive } from '@angular/core';
2
+ import { effect, InjectionToken, signal, inject, Injector, DestroyRef, ElementRef, input, booleanAttribute, computed, output, afterNextRender, Directive } from '@angular/core';
3
3
  import { getActiveElement, createContext } from '@radix-ng/primitives/core';
4
4
 
5
- const RdxFocusScopeConfigToken = new InjectionToken('RdxFocusScopeConfig', {
6
- factory: () => ({
7
- trapped: signal(false)
8
- })
9
- });
10
- function provideRdxFocusScopeConfig(factory) {
11
- return { provide: RdxFocusScopeConfigToken, useFactory: factory };
12
- }
13
-
14
- function createGlobalState(factory) {
15
- const state = factory();
16
- return () => state;
5
+ const AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount';
6
+ const AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount';
7
+ const EVENT_OPTIONS = { bubbles: false, cancelable: true };
8
+ /**
9
+ * The real target of a (possibly retargeted) event, piercing shadow boundaries via `composedPath()`.
10
+ * Falls back to `event.target` when `composedPath` is unavailable.
11
+ */
12
+ function getEventTarget(event) {
13
+ return event.composedPath?.()[0] ?? event.target;
17
14
  }
18
- const useFocusStackState = createGlobalState(() => signal([]));
19
- function createFocusScopesStack() {
20
- /** A stack of focus scopes, with the active one at the top */
21
- const stack = useFocusStackState();
22
- return {
23
- add(focusScope) {
24
- const current = stack();
25
- const active = current[0];
26
- if (focusScope !== active) {
27
- active?.pause();
28
- }
29
- const updated = arrayRemove(current, focusScope);
30
- updated.unshift(focusScope);
31
- stack.set(updated);
32
- },
33
- remove(focusScope) {
34
- const current = stack();
35
- const updated = arrayRemove(current, focusScope);
36
- stack.set(updated);
37
- // после удаления «возобновляем» новый верхний
38
- stack()[0]?.resume();
15
+ /**
16
+ * Shadow-DOM-aware containment: whether `node` is `container` or lives inside it, crossing shadow roots
17
+ * via their `host` (unlike `Node.contains`, which stops at a shadow boundary).
18
+ */
19
+ function composedContains(container, node) {
20
+ let current = node;
21
+ while (current) {
22
+ if (current === container) {
23
+ return true;
39
24
  }
40
- };
41
- }
42
- function arrayRemove(array, item) {
43
- const copy = [...array];
44
- const idx = copy.indexOf(item);
45
- if (idx !== -1) {
46
- copy.splice(idx, 1);
25
+ current = current instanceof ShadowRoot ? current.host : current.parentNode;
47
26
  }
48
- return copy;
49
- }
50
- function removeLinks(items) {
51
- return items.filter((el) => el.tagName !== 'A');
27
+ return false;
52
28
  }
53
-
54
- const AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount';
55
- const AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount';
56
- const EVENT_OPTIONS = { bubbles: false, cancelable: true };
57
29
  /**
58
30
  * Attempts focusing the first element in a list of candidates.
59
31
  * Stops when focus has actually moved.
@@ -79,7 +51,7 @@ function focusFirst(candidates, { select = false } = {}) {
79
51
  */
80
52
  function getTabbableCandidates(container) {
81
53
  const nodes = [];
82
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
54
+ const walker = container.ownerDocument.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
83
55
  acceptNode: (node) => {
84
56
  const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden';
85
57
  if (node.disabled || node.hidden || isHiddenInput)
@@ -97,13 +69,17 @@ function getTabbableCandidates(container) {
97
69
  return nodes;
98
70
  }
99
71
  function isHidden(node, { upTo }) {
100
- if (getComputedStyle(node).visibility === 'hidden')
72
+ const view = node.ownerDocument.defaultView;
73
+ if (!view) {
74
+ return false; // no view (detached / SSR) — cannot resolve computed styles, treat as visible
75
+ }
76
+ if (view.getComputedStyle(node).visibility === 'hidden')
101
77
  return true;
102
78
  while (node) {
103
79
  // we stop at `upTo` (excluding it)
104
80
  if (upTo !== undefined && node === upTo)
105
81
  return false;
106
- if (getComputedStyle(node).display === 'none')
82
+ if (view.getComputedStyle(node).display === 'none')
107
83
  return true;
108
84
  node = node.parentElement;
109
85
  }
@@ -130,6 +106,54 @@ function getTabbableEdges(container) {
130
106
  const last = findVisible(candidates.reverse(), container);
131
107
  return [first, last];
132
108
  }
109
+ /** Visible tabbable elements of `root` in document order (the basis for tab-order navigation). */
110
+ function visibleTabbablesIn(root) {
111
+ return getTabbableCandidates(root).filter((el) => !isHidden(el, { upTo: root }));
112
+ }
113
+ /** The tabbable one step (`dir`) from the document's active element, within `container`. */
114
+ function getTabbableIn(container, dir) {
115
+ const list = visibleTabbablesIn(container);
116
+ if (list.length === 0) {
117
+ return undefined;
118
+ }
119
+ const active = getActiveElement(container.ownerDocument);
120
+ const index = active ? list.indexOf(active) : -1;
121
+ const nextIndex = index === -1 ? (dir === 1 ? 0 : list.length - 1) : index + dir;
122
+ return list[nextIndex];
123
+ }
124
+ /**
125
+ * The next tabbable in the document after the current focus (Base UI `getNextTabbable`) — used by the
126
+ * portal-focus bridge's trailing guard to step focus past the popup. Falls back to `reference`.
127
+ */
128
+ function getNextTabbable(reference) {
129
+ const body = (reference?.ownerDocument ?? document).body;
130
+ return getTabbableIn(body, 1) ?? reference;
131
+ }
132
+ /** The previous tabbable in the document before the current focus (Base UI `getPreviousTabbable`). */
133
+ function getPreviousTabbable(reference) {
134
+ const body = (reference?.ownerDocument ?? document).body;
135
+ return getTabbableIn(body, -1) ?? reference;
136
+ }
137
+ /** The tabbable `dir` steps from `reference` in the document, wrapping around. */
138
+ function getTabbableNearElement(reference, dir) {
139
+ if (!reference) {
140
+ return null;
141
+ }
142
+ const list = visibleTabbablesIn(reference.ownerDocument.body);
143
+ const index = list.indexOf(reference);
144
+ if (list.length === 0 || index === -1) {
145
+ return null;
146
+ }
147
+ return list[(index + dir + list.length) % list.length];
148
+ }
149
+ /** The tabbable immediately after `reference` in the document, wrapping (Base UI `getTabbableAfterElement`). */
150
+ function getTabbableAfterElement(reference) {
151
+ return getTabbableNearElement(reference, 1);
152
+ }
153
+ /** The tabbable immediately before `reference` in the document, wrapping (Base UI `getTabbableBeforeElement`). */
154
+ function getTabbableBeforeElement(reference) {
155
+ return getTabbableNearElement(reference, -1);
156
+ }
133
157
  function isSelectableInput(element) {
134
158
  return element instanceof HTMLInputElement && 'select' in element;
135
159
  }
@@ -146,6 +170,181 @@ function focus(element, { select = false } = {}) {
146
170
  }
147
171
  }
148
172
 
173
+ /** Marks the leading / trailing focus-guard spans (Base UI `data-base-ui-focus-guard`). */
174
+ const FOCUS_GUARD_ATTR = 'data-rdx-focus-guard';
175
+ /** Saved-tabindex marker used by {@link disableFocusInside} / {@link enableFocusInside}. */
176
+ const SAVED_TABINDEX_ATTR = 'data-rdx-tabindex';
177
+ /** Visually-hidden, off-flow style for a focus guard / `aria-owns` anchor (Base UI `visuallyHidden`). */
178
+ const FOCUS_GUARD_STYLE = {
179
+ position: 'fixed',
180
+ top: '0',
181
+ left: '0',
182
+ width: '1px',
183
+ height: '1px',
184
+ padding: '0',
185
+ margin: '-1px',
186
+ overflow: 'hidden',
187
+ clipPath: 'inset(50%)',
188
+ whiteSpace: 'nowrap',
189
+ border: '0'
190
+ };
191
+ /**
192
+ * Creates a visually-hidden, **tabbable** focus-guard `<span>` — the Angular counterpart of Base UI's
193
+ * `FocusGuard`. The portal-focus bridge places one before and one after the portal content so a Tab into
194
+ * (or out of) the portal lands on a guard, which then redirects focus to the right boundary.
195
+ */
196
+ function createFocusGuard(ownerDocument) {
197
+ const guard = ownerDocument.createElement('span');
198
+ guard.setAttribute('tabindex', '0');
199
+ guard.setAttribute('aria-hidden', 'true');
200
+ guard.setAttribute(FOCUS_GUARD_ATTR, '');
201
+ Object.assign(guard.style, FOCUS_GUARD_STYLE);
202
+ return guard;
203
+ }
204
+ /**
205
+ * Creates a visually-hidden `<span aria-owns="…">` that links the portal node into the trigger's tab /
206
+ * AT order (Base UI's single `aria-owns` anchor). The manager places it next to the trigger.
207
+ */
208
+ function createAriaOwnsAnchor(ownerDocument, portalId) {
209
+ const anchor = ownerDocument.createElement('span');
210
+ anchor.setAttribute('aria-owns', portalId);
211
+ Object.assign(anchor.style, FOCUS_GUARD_STYLE);
212
+ return anchor;
213
+ }
214
+ /**
215
+ * Makes every tabbable descendant of `container` **non-tabbable** (`tabindex="-1"`), saving each one's
216
+ * original tabindex so {@link enableFocusInside} can restore it. Base UI `disableFocusInside`: a
217
+ * non-modal portal keeps its content untabbable until focus is actually inside it, so a Tab from the
218
+ * trigger steps onto the guard instead of jumping into the content.
219
+ */
220
+ function disableFocusInside(container) {
221
+ for (const element of getTabbableCandidates(container)) {
222
+ element.setAttribute(SAVED_TABINDEX_ATTR, element.getAttribute('tabindex') ?? '');
223
+ element.setAttribute('tabindex', '-1');
224
+ }
225
+ }
226
+ /** Restores the tabbability that {@link disableFocusInside} suspended. Base UI `enableFocusInside`. */
227
+ function enableFocusInside(container) {
228
+ container.querySelectorAll(`[${SAVED_TABINDEX_ATTR}]`).forEach((element) => {
229
+ const original = element.getAttribute(SAVED_TABINDEX_ATTR);
230
+ element.removeAttribute(SAVED_TABINDEX_ATTR);
231
+ if (original) {
232
+ element.setAttribute('tabindex', original);
233
+ }
234
+ else {
235
+ element.removeAttribute('tabindex');
236
+ }
237
+ });
238
+ }
239
+ /**
240
+ * Whether a focus event crossed the `container` boundary — its `relatedTarget` (the other side of the
241
+ * focus move) is `null` or outside `container` (Base UI `isOutsideEvent`). Shadow-DOM-aware via
242
+ * {@link composedContains}.
243
+ */
244
+ function isOutsideEvent(event, container) {
245
+ const relatedTarget = event.relatedTarget;
246
+ return !relatedTarget || !composedContains(container, relatedTarget);
247
+ }
248
+ /**
249
+ * The portal-focus bridge's **tabbability toggle** (Base UI `FloatingPortal` capture-phase `onFocus`).
250
+ * While `portalNode` is mounted (and `enabled`), it makes the portal content tabbable **only when focus
251
+ * is inside it**: focus entering from outside re-enables tabbability, focus leaving to outside disables
252
+ * it again. Listens on the **capture** phase so it settles before the focus manager's guards react.
253
+ *
254
+ * Must be called in an injection context. The initial disable-on-mount and the guard-span placement are
255
+ * the manager's responsibility (Phase 1b); this owns only the dynamic in/out toggle.
256
+ */
257
+ function useFocusGuardsTabbability(portalNode, options = {}) {
258
+ const enabled = options.enabled ?? (() => true);
259
+ let focusInsideDisabled = false;
260
+ effect((onCleanup) => {
261
+ const node = portalNode();
262
+ if (!node || !enabled()) {
263
+ return;
264
+ }
265
+ const onFocus = (event) => {
266
+ // Only react to focus actually crossing the portal boundary.
267
+ if (!event.relatedTarget || !isOutsideEvent(event, node)) {
268
+ return;
269
+ }
270
+ if (event.type === 'focusin') {
271
+ if (focusInsideDisabled) {
272
+ enableFocusInside(node);
273
+ focusInsideDisabled = false;
274
+ }
275
+ }
276
+ else {
277
+ disableFocusInside(node);
278
+ focusInsideDisabled = true;
279
+ }
280
+ };
281
+ node.addEventListener('focusin', onFocus, true);
282
+ node.addEventListener('focusout', onFocus, true);
283
+ onCleanup(() => {
284
+ node.removeEventListener('focusin', onFocus, true);
285
+ node.removeEventListener('focusout', onFocus, true);
286
+ });
287
+ });
288
+ }
289
+
290
+ const RdxFocusScopeConfigToken = new InjectionToken('RdxFocusScopeConfig', {
291
+ factory: () => ({
292
+ trapped: signal(false)
293
+ })
294
+ });
295
+ function provideRdxFocusScopeConfig(factory) {
296
+ return { provide: RdxFocusScopeConfigToken, useFactory: factory };
297
+ }
298
+
299
+ /**
300
+ * The active-scope stack pauses/resumes scopes, so it **is** cross-document coordination state — keyed
301
+ * per owner `Document` (a `WeakMap`) rather than process-global (ADR 0017 Phase 1a): opening a scope in
302
+ * document B must not pause document A's scope.
303
+ */
304
+ const stacksByDocument = new WeakMap();
305
+ function getFocusStackState(document) {
306
+ let state = stacksByDocument.get(document);
307
+ if (!state) {
308
+ state = signal([]);
309
+ stacksByDocument.set(document, state);
310
+ }
311
+ return state;
312
+ }
313
+ function createFocusScopesStack(document) {
314
+ /** A stack of focus scopes for this document, with the active one at the top */
315
+ const stack = getFocusStackState(document);
316
+ return {
317
+ add(focusScope) {
318
+ const current = stack();
319
+ const active = current[0];
320
+ if (focusScope !== active) {
321
+ active?.pause();
322
+ }
323
+ const updated = arrayRemove(current, focusScope);
324
+ updated.unshift(focusScope);
325
+ stack.set(updated);
326
+ },
327
+ remove(focusScope) {
328
+ const current = stack();
329
+ const updated = arrayRemove(current, focusScope);
330
+ stack.set(updated);
331
+ // после удаления «возобновляем» новый верхний
332
+ stack()[0]?.resume();
333
+ }
334
+ };
335
+ }
336
+ function arrayRemove(array, item) {
337
+ const copy = [...array];
338
+ const idx = copy.indexOf(item);
339
+ if (idx !== -1) {
340
+ copy.splice(idx, 1);
341
+ }
342
+ return copy;
343
+ }
344
+ function removeLinks(items) {
345
+ return items.filter((el) => el.tagName !== 'A');
346
+ }
347
+
149
348
  const [injectFocusScopeContext, provideFocusScopeContext] = createContext('FocusScope Context', 'utils/focus-scope');
150
349
  const rootContext = () => {
151
350
  const context = inject(RdxFocusScope);
@@ -163,6 +362,8 @@ class RdxFocusScope {
163
362
  this.destroyRef = inject(DestroyRef);
164
363
  this.elementRef = inject(ElementRef);
165
364
  this.config = inject(RdxFocusScopeConfigToken);
365
+ /** The host's owner `Document` — all focus listeners / reads are scoped here, never global `document`. */
366
+ this.ownerDocument = this.elementRef.nativeElement.ownerDocument ?? document;
166
367
  /**
167
368
  * When `true`, tabbing from last item will focus first tabbable
168
369
  * and shift+tab from first item will focus last tababble.
@@ -195,7 +396,7 @@ class RdxFocusScope {
195
396
  */
196
397
  this.unmountAutoFocus = output();
197
398
  this.lastFocusedElement = signal(null, ...(ngDevMode ? [{ debugName: "lastFocusedElement" }] : /* istanbul ignore next */ []));
198
- this.focusScopesStack = createFocusScopesStack();
399
+ this.focusScopesStack = createFocusScopesStack(this.ownerDocument);
199
400
  this.focusScope = {
200
401
  paused: signal(false),
201
402
  pause: () => this.focusScope.paused.set(true),
@@ -213,8 +414,8 @@ class RdxFocusScope {
213
414
  if (this.focusScope.paused() || !container) {
214
415
  return;
215
416
  }
216
- const target = event.target;
217
- if (this.elementRef.nativeElement.contains(target)) {
417
+ const target = getEventTarget(event);
418
+ if (composedContains(container, target)) {
218
419
  this.lastFocusedElement.set(target);
219
420
  }
220
421
  else {
@@ -240,12 +441,12 @@ class RdxFocusScope {
240
441
  return;
241
442
  // If the focus has moved to an actual legitimate element (`relatedTarget !== null`)
242
443
  // that is outside the container, we move focus to the last valid focused element inside.
243
- if (!container.contains(relatedTarget)) {
444
+ if (!composedContains(container, relatedTarget)) {
244
445
  focus(this.lastFocusedElement(), { select: true });
245
446
  }
246
447
  };
247
448
  const handleMutations = () => {
248
- const isLastFocusedElementExist = container.contains(this.lastFocusedElement());
449
+ const isLastFocusedElementExist = composedContains(container, this.lastFocusedElement());
249
450
  if (!isLastFocusedElementExist) {
250
451
  focus(container);
251
452
  }
@@ -254,11 +455,11 @@ class RdxFocusScope {
254
455
  if (container) {
255
456
  mutationObserver.observe(container, { childList: true, subtree: true });
256
457
  }
257
- document.addEventListener('focusin', handleFocusIn);
258
- document.addEventListener('focusout', handleFocusOut);
458
+ this.ownerDocument.addEventListener('focusin', handleFocusIn);
459
+ this.ownerDocument.addEventListener('focusout', handleFocusOut);
259
460
  onCleanup(() => {
260
- document.removeEventListener('focusin', handleFocusIn);
261
- document.removeEventListener('focusout', handleFocusOut);
461
+ this.ownerDocument.removeEventListener('focusin', handleFocusIn);
462
+ this.ownerDocument.removeEventListener('focusout', handleFocusOut);
262
463
  mutationObserver.disconnect();
263
464
  });
264
465
  }
@@ -270,8 +471,8 @@ class RdxFocusScope {
270
471
  return;
271
472
  }
272
473
  this.focusScopesStack.add(this.focusScope);
273
- const previouslyFocusedElement = getActiveElement();
274
- const hasFocusedCandidate = container.contains(previouslyFocusedElement);
474
+ const previouslyFocusedElement = getActiveElement(this.ownerDocument);
475
+ const hasFocusedCandidate = composedContains(container, previouslyFocusedElement);
275
476
  const mountEventHandler = (ev) => {
276
477
  if (this.alive)
277
478
  this.mountAutoFocus.emit(ev);
@@ -284,7 +485,7 @@ class RdxFocusScope {
284
485
  focusFirst(removeLinks(getTabbableCandidates(container)), {
285
486
  select: true
286
487
  });
287
- if (getActiveElement() === previouslyFocusedElement)
488
+ if (getActiveElement(this.ownerDocument) === previouslyFocusedElement)
288
489
  focus(container);
289
490
  }
290
491
  }
@@ -297,20 +498,45 @@ class RdxFocusScope {
297
498
  container.removeEventListener(AUTOFOCUS_ON_MOUNT, mountEventHandler);
298
499
  const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS);
299
500
  container.dispatchEvent(unmountEvent);
300
- setTimeout(() => {
301
- if (!unmountEvent.defaultPrevented)
302
- focus(previouslyFocusedElement ?? document.body, { select: true });
501
+ // Queue the return-focus on the owner window's animation frame (not `setTimeout`),
502
+ // so it runs after the unmounting paint settles (ADR 0017 Phase 1a queued focus).
503
+ const view = this.ownerDocument.defaultView ?? globalThis;
504
+ view.requestAnimationFrame(() => {
505
+ // An enclosing focus manager can override the return target (ADR 0017
506
+ // `returnFocus`): `false` suppresses it, an element returns there explicitly
507
+ // (bypassing the moved-focus guard), `undefined` keeps the default behavior.
508
+ const override = this.config.returnFocus?.();
509
+ if (override !== false && !unmountEvent.defaultPrevented) {
510
+ if (override) {
511
+ focus(override, { select: true });
512
+ }
513
+ else if (!this.shouldPreserveMovedFocus()) {
514
+ focus(previouslyFocusedElement ?? this.ownerDocument.body, { select: true });
515
+ }
516
+ }
303
517
  // we need to remove the listener after we `dispatchEvent`
304
518
  container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, unmountEventHandler);
305
519
  this.focusScopesStack.remove(this.focusScope);
306
- }, 0);
520
+ });
307
521
  });
308
522
  }, { injector: this.injector });
309
523
  });
310
524
  }
525
+ /**
526
+ * Whether the interaction that unmounted this scope already moved focus to a legitimate element
527
+ * **outside** it — e.g. an outside press onto an interactive control in a non-modal layer (ADR 0017
528
+ * §2, finding #3). Returning focus to the previously-focused element would then *steal* it back from
529
+ * what the user just acted on. Focus that fell to `<body>` / `null` (a backdrop press, Escape, or the
530
+ * focused element being removed) is **not** "moved" — return focus normally so keyboard users land
531
+ * back on the trigger. The page never scroll-jumps either way: {@link focus} uses `preventScroll`.
532
+ */
533
+ shouldPreserveMovedFocus() {
534
+ const active = getActiveElement(this.ownerDocument);
535
+ return (!!active && active !== this.ownerDocument.body && !composedContains(this.elementRef.nativeElement, active));
536
+ }
311
537
  handleKeyDown(event) {
312
538
  const isTabKey = event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey;
313
- const focusedElement = getActiveElement();
539
+ const focusedElement = getActiveElement(this.ownerDocument);
314
540
  if (isTabKey && focusedElement) {
315
541
  const container = event.currentTarget;
316
542
  const [first, last] = getTabbableEdges(container);
@@ -355,5 +581,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
355
581
  * Generated bundle index. Do not edit.
356
582
  */
357
583
 
358
- export { RdxFocusScope, RdxFocusScopeConfigToken, injectFocusScopeContext, provideFocusScopeContext, provideRdxFocusScopeConfig };
584
+ export { AUTOFOCUS_ON_MOUNT, AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS, FOCUS_GUARD_ATTR, FOCUS_GUARD_STYLE, RdxFocusScope, RdxFocusScopeConfigToken, composedContains, createAriaOwnsAnchor, createFocusGuard, disableFocusInside, enableFocusInside, findVisible, focus, focusFirst, getEventTarget, getNextTabbable, getPreviousTabbable, getTabbableAfterElement, getTabbableBeforeElement, getTabbableCandidates, getTabbableEdges, injectFocusScopeContext, isHidden, isOutsideEvent, isSelectableInput, provideFocusScopeContext, provideRdxFocusScopeConfig, useFocusGuardsTabbability };
359
585
  //# sourceMappingURL=radix-ng-primitives-focus-scope.mjs.map