@radix-ng/primitives 1.0.9 → 1.0.10
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.
- package/fesm2022/radix-ng-primitives-autocomplete.mjs +11 -3
- package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-combobox.mjs +11 -3
- package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-core.mjs +9 -6
- package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-date-field.mjs +18 -8
- package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +9 -0
- package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-focus-scope.mjs +241 -95
- package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-menu.mjs +97 -9
- package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs +17 -3
- package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-popper.mjs +148 -23
- package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-portal.mjs +16 -5
- package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-select.mjs +14 -6
- package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-time-field.mjs +12 -4
- package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
- package/package.json +1 -5
- package/types/radix-ng-primitives-core.d.ts +15 -12
- package/types/radix-ng-primitives-date-field.d.ts +14 -4
- package/types/radix-ng-primitives-floating-focus-manager.d.ts +7 -0
- package/types/radix-ng-primitives-focus-scope.d.ts +61 -44
- package/types/radix-ng-primitives-menu.d.ts +14 -3
- package/types/radix-ng-primitives-popper.d.ts +136 -43
- package/types/radix-ng-primitives-portal.d.ts +18 -8
- package/types/radix-ng-primitives-time-field.d.ts +12 -4
- package/fesm2022/radix-ng-primitives-focus-guards.mjs +0 -53
- package/fesm2022/radix-ng-primitives-focus-guards.mjs.map +0 -1
- package/focus-guards/README.md +0 -1
- package/types/radix-ng-primitives-focus-guards.d.ts +0 -15
|
@@ -2,19 +2,31 @@ import * as i0 from '@angular/core';
|
|
|
2
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
|
|
6
|
-
const
|
|
7
|
-
const
|
|
5
|
+
const CANDIDATE_SELECTOR = 'a[href],button,input,select,textarea,summary,details,iframe,object,embed,[tabindex],[contenteditable]:not([contenteditable="false"]),audio[controls],video[controls]';
|
|
6
|
+
const getNodeName = (node) => node.nodeName.toLowerCase();
|
|
7
|
+
const isNode = (value) => value != null && typeof value.nodeType === 'number';
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Owner-window-safe `HTMLElement` check. A raw `value instanceof HTMLElement` is realm-sensitive — it
|
|
10
|
+
* returns `false` for an element from another document (iframe / popup window), whose realm has its own
|
|
11
|
+
* `HTMLElement` constructor. Resolve the constructor from the node's own `defaultView` (the same pattern
|
|
12
|
+
* as `dismissable-layer`'s `dismiss.ts`; Base UI `isHTMLElement`).
|
|
11
13
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const isHTMLElement = (value) => {
|
|
15
|
+
if (!isNode(value)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const view = value.ownerDocument?.defaultView;
|
|
19
|
+
return view ? value instanceof view.HTMLElement : value instanceof HTMLElement;
|
|
20
|
+
};
|
|
21
|
+
/** A shadow root, detected structurally (`DOCUMENT_FRAGMENT_NODE` with a `host`) so it is realm- and SSR-safe. */
|
|
22
|
+
const isShadowRoot = (node) => node !== null && node.nodeType === 11 && 'host' in node;
|
|
23
|
+
const getComputedStyleOf = (element) => {
|
|
24
|
+
const view = element.ownerDocument.defaultView;
|
|
25
|
+
return view ? view.getComputedStyle(element) : null;
|
|
26
|
+
};
|
|
15
27
|
/**
|
|
16
28
|
* 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).
|
|
29
|
+
* via their `host` (unlike `Node.contains`, which stops at a shadow boundary). Base UI `contains`.
|
|
18
30
|
*/
|
|
19
31
|
function composedContains(container, node) {
|
|
20
32
|
let current = node;
|
|
@@ -22,97 +34,190 @@ function composedContains(container, node) {
|
|
|
22
34
|
if (current === container) {
|
|
23
35
|
return true;
|
|
24
36
|
}
|
|
25
|
-
current = current
|
|
37
|
+
current = isShadowRoot(current) ? current.host : current.parentNode;
|
|
26
38
|
}
|
|
27
39
|
return false;
|
|
28
40
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
for (const candidate of candidates) {
|
|
36
|
-
focus(candidate, { select });
|
|
37
|
-
if (getActiveElement() !== previouslyFocusedElement)
|
|
38
|
-
return true;
|
|
41
|
+
function isHiddenByStyles(styles) {
|
|
42
|
+
return styles.visibility === 'hidden' || styles.visibility === 'collapse';
|
|
43
|
+
}
|
|
44
|
+
function isElementVisible(element, styles) {
|
|
45
|
+
if (!element.isConnected || !styles || isHiddenByStyles(styles)) {
|
|
46
|
+
return false;
|
|
39
47
|
}
|
|
40
|
-
|
|
48
|
+
if (typeof element.checkVisibility === 'function') {
|
|
49
|
+
return element.checkVisibility();
|
|
50
|
+
}
|
|
51
|
+
return styles.display !== 'none' && styles.display !== 'contents';
|
|
41
52
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return NodeFilter.FILTER_SKIP;
|
|
59
|
-
// `.tabIndex` is not the same as the `tabindex` attribute. It works on the
|
|
60
|
-
// runtime's understanding of tabbability, so this automatically accounts
|
|
61
|
-
// for any kind of element that could be tabbed to.
|
|
62
|
-
return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
53
|
+
/** The composed parent: an assigned `<slot>`, the element's parent, or the shadow host at a boundary. */
|
|
54
|
+
function getParentElement(element) {
|
|
55
|
+
const assignedSlot = element.assignedSlot;
|
|
56
|
+
if (assignedSlot) {
|
|
57
|
+
return assignedSlot;
|
|
58
|
+
}
|
|
59
|
+
if (element.parentElement) {
|
|
60
|
+
return element.parentElement;
|
|
61
|
+
}
|
|
62
|
+
const rootNode = element.getRootNode();
|
|
63
|
+
return isShadowRoot(rootNode) ? rootNode.host : null;
|
|
64
|
+
}
|
|
65
|
+
function getDetailsSummary(details) {
|
|
66
|
+
for (const child of Array.from(details.children)) {
|
|
67
|
+
if (getNodeName(child) === 'summary') {
|
|
68
|
+
return child;
|
|
63
69
|
}
|
|
64
|
-
});
|
|
65
|
-
while (walker.nextNode())
|
|
66
|
-
nodes.push(walker.currentNode);
|
|
67
|
-
// we do not take into account the order of nodes with positive `tabIndex` as it
|
|
68
|
-
// hinders accessibility to have tab order different from visual order.
|
|
69
|
-
return nodes;
|
|
70
|
-
}
|
|
71
|
-
function isHidden(node, { upTo }) {
|
|
72
|
-
const view = node.ownerDocument.defaultView;
|
|
73
|
-
if (!view) {
|
|
74
|
-
return false; // no view (detached / SSR) — cannot resolve computed styles, treat as visible
|
|
75
70
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function isWithinOpenDetailsSummary(element, details) {
|
|
74
|
+
const summary = getDetailsSummary(details);
|
|
75
|
+
return !!summary && (element === summary || composedContains(summary, element));
|
|
76
|
+
}
|
|
77
|
+
function isFocusableCandidate(element) {
|
|
78
|
+
const nodeName = element ? getNodeName(element) : '';
|
|
79
|
+
return (element != null &&
|
|
80
|
+
element.matches(CANDIDATE_SELECTOR) &&
|
|
81
|
+
// a `<summary>` is only focusable as the (first) summary of a `<details>`
|
|
82
|
+
(nodeName !== 'summary' ||
|
|
83
|
+
(element.parentElement != null &&
|
|
84
|
+
getNodeName(element.parentElement) === 'details' &&
|
|
85
|
+
getDetailsSummary(element.parentElement) === element)) &&
|
|
86
|
+
// a `<details>` with its own summary is not focusable (the summary is)
|
|
87
|
+
(nodeName !== 'details' || getDetailsSummary(element) == null) &&
|
|
88
|
+
(nodeName !== 'input' || element.type !== 'hidden'));
|
|
89
|
+
}
|
|
90
|
+
function isVisibleInTabbableTree(element, isAncestor) {
|
|
91
|
+
const styles = getComputedStyleOf(element);
|
|
92
|
+
if (!isAncestor) {
|
|
93
|
+
return isElementVisible(element, styles);
|
|
94
|
+
}
|
|
95
|
+
return !!styles && styles.display !== 'none';
|
|
96
|
+
}
|
|
97
|
+
function isFocusableElement(element) {
|
|
98
|
+
if (!isFocusableCandidate(element) || !element.isConnected || element.matches(':disabled')) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
for (let current = element; current; current = getParentElement(current)) {
|
|
102
|
+
const isAncestor = current !== element;
|
|
103
|
+
const isSlot = getNodeName(current) === 'slot';
|
|
104
|
+
if (current.hasAttribute('inert')) {
|
|
81
105
|
return false;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
}
|
|
107
|
+
if ((isAncestor &&
|
|
108
|
+
getNodeName(current) === 'details' &&
|
|
109
|
+
!current.open &&
|
|
110
|
+
!isWithinOpenDetailsSummary(element, current)) ||
|
|
111
|
+
current.hasAttribute('hidden') ||
|
|
112
|
+
(!isSlot && !isVisibleInTabbableTree(current, isAncestor))) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
85
115
|
}
|
|
86
|
-
return
|
|
116
|
+
return true;
|
|
87
117
|
}
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
/** The effective tab index, giving `<details>`/media/`contenteditable` their implicit `0`. */
|
|
119
|
+
function getTabIndex(element) {
|
|
120
|
+
const tabIndex = element.tabIndex;
|
|
121
|
+
if (tabIndex < 0) {
|
|
122
|
+
const nodeName = getNodeName(element);
|
|
123
|
+
if (nodeName === 'details' ||
|
|
124
|
+
nodeName === 'audio' ||
|
|
125
|
+
nodeName === 'video' ||
|
|
126
|
+
(isHTMLElement(element) && element.isContentEditable)) {
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return tabIndex;
|
|
131
|
+
}
|
|
132
|
+
function getNamedRadioInput(element) {
|
|
133
|
+
if (getNodeName(element) !== 'input') {
|
|
134
|
+
return null;
|
|
97
135
|
}
|
|
98
|
-
|
|
136
|
+
const input = element;
|
|
137
|
+
return input.type === 'radio' && input.name !== '' ? input : null;
|
|
138
|
+
}
|
|
139
|
+
/** Within a named radio group only the checked radio (or the first, if none is checked) is tabbable. */
|
|
140
|
+
function isTabbableRadio(element, candidates) {
|
|
141
|
+
const input = getNamedRadioInput(element);
|
|
142
|
+
if (!input) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
const checkedRadio = candidates.find((candidate) => {
|
|
146
|
+
const radio = getNamedRadioInput(candidate);
|
|
147
|
+
return radio?.name === input.name && radio.form === input.form && radio.checked;
|
|
148
|
+
});
|
|
149
|
+
if (checkedRadio) {
|
|
150
|
+
return checkedRadio === input;
|
|
151
|
+
}
|
|
152
|
+
return (candidates.find((candidate) => {
|
|
153
|
+
const radio = getNamedRadioInput(candidate);
|
|
154
|
+
return radio?.name === input.name && radio.form === input.form;
|
|
155
|
+
}) === input);
|
|
156
|
+
}
|
|
157
|
+
/** The composed children of a node: `<slot>` assigned elements, then shadow-root children, else children. */
|
|
158
|
+
function getComposedChildren(container) {
|
|
159
|
+
if (isHTMLElement(container) && getNodeName(container) === 'slot') {
|
|
160
|
+
const assignedElements = container.assignedElements({ flatten: true });
|
|
161
|
+
if (assignedElements.length > 0) {
|
|
162
|
+
return assignedElements;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (isHTMLElement(container) && container.shadowRoot) {
|
|
166
|
+
return Array.from(container.shadowRoot.children);
|
|
167
|
+
}
|
|
168
|
+
return Array.from(container.children);
|
|
169
|
+
}
|
|
170
|
+
function appendCandidates(container, list) {
|
|
171
|
+
getComposedChildren(container).forEach((child) => {
|
|
172
|
+
if (isFocusableCandidate(child)) {
|
|
173
|
+
list.push(child);
|
|
174
|
+
}
|
|
175
|
+
appendCandidates(child, list);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** Collects elements matching `selector` across the composed tree (shadow / slot aware). */
|
|
179
|
+
function queryComposedAll(container, selector) {
|
|
180
|
+
const list = [];
|
|
181
|
+
const walk = (node) => {
|
|
182
|
+
getComposedChildren(node).forEach((child) => {
|
|
183
|
+
if (isHTMLElement(child) && child.matches(selector)) {
|
|
184
|
+
list.push(child);
|
|
185
|
+
}
|
|
186
|
+
walk(child);
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
walk(container);
|
|
190
|
+
return list;
|
|
191
|
+
}
|
|
192
|
+
/** Whether `element` is reachable by Tab right now (Base UI `isTabbable`). */
|
|
193
|
+
function isTabbable(element) {
|
|
194
|
+
return isFocusableElement(element) && getTabIndex(element) >= 0;
|
|
195
|
+
}
|
|
196
|
+
/** All programmatically focusable elements inside `container`, in composed document order. */
|
|
197
|
+
function focusable(container) {
|
|
198
|
+
const candidates = [];
|
|
199
|
+
appendCandidates(container, candidates);
|
|
200
|
+
return candidates.filter(isFocusableElement);
|
|
201
|
+
}
|
|
202
|
+
/** All keyboard-tabbable elements inside `container`, in composed document order (Base UI `tabbable`). */
|
|
203
|
+
function tabbable(container) {
|
|
204
|
+
const candidates = focusable(container);
|
|
205
|
+
return candidates.filter((element) => getTabIndex(element) >= 0 && isTabbableRadio(element, candidates));
|
|
99
206
|
}
|
|
100
207
|
/**
|
|
101
|
-
*
|
|
208
|
+
* Back-compat alias for {@link tabbable} — historically returned a looser `TreeWalker` approximation;
|
|
209
|
+
* now the full Base UI tabbable set. Kept so existing imports (`getTabbableCandidates`) keep working.
|
|
102
210
|
*/
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
const first = findVisible(candidates, container);
|
|
106
|
-
const last = findVisible(candidates.reverse(), container);
|
|
107
|
-
return [first, last];
|
|
211
|
+
function getTabbableCandidates(container) {
|
|
212
|
+
return tabbable(container);
|
|
108
213
|
}
|
|
109
|
-
/**
|
|
110
|
-
function
|
|
111
|
-
|
|
214
|
+
/** The first and last tabbable elements inside `container`. */
|
|
215
|
+
function getTabbableEdges(container) {
|
|
216
|
+
const list = tabbable(container);
|
|
217
|
+
return [list[0], list[list.length - 1]];
|
|
112
218
|
}
|
|
113
|
-
/** The tabbable one step (`dir`) from the document's active element, within `container`. */
|
|
114
219
|
function getTabbableIn(container, dir) {
|
|
115
|
-
const list =
|
|
220
|
+
const list = tabbable(container);
|
|
116
221
|
if (list.length === 0) {
|
|
117
222
|
return undefined;
|
|
118
223
|
}
|
|
@@ -134,12 +239,11 @@ function getPreviousTabbable(reference) {
|
|
|
134
239
|
const body = (reference?.ownerDocument ?? document).body;
|
|
135
240
|
return getTabbableIn(body, -1) ?? reference;
|
|
136
241
|
}
|
|
137
|
-
/** The tabbable `dir` steps from `reference` in the document, wrapping around. */
|
|
138
242
|
function getTabbableNearElement(reference, dir) {
|
|
139
243
|
if (!reference) {
|
|
140
244
|
return null;
|
|
141
245
|
}
|
|
142
|
-
const list =
|
|
246
|
+
const list = tabbable(reference.ownerDocument.body);
|
|
143
247
|
const index = list.indexOf(reference);
|
|
144
248
|
if (list.length === 0 || index === -1) {
|
|
145
249
|
return null;
|
|
@@ -154,13 +258,45 @@ function getTabbableAfterElement(reference) {
|
|
|
154
258
|
function getTabbableBeforeElement(reference) {
|
|
155
259
|
return getTabbableNearElement(reference, -1);
|
|
156
260
|
}
|
|
261
|
+
|
|
262
|
+
const AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount';
|
|
263
|
+
const AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount';
|
|
264
|
+
const EVENT_OPTIONS = { bubbles: false, cancelable: true };
|
|
265
|
+
/**
|
|
266
|
+
* The real target of a (possibly retargeted) event, piercing shadow boundaries via `composedPath()`.
|
|
267
|
+
* Falls back to `event.target` when `composedPath` is unavailable.
|
|
268
|
+
*/
|
|
269
|
+
function getEventTarget(event) {
|
|
270
|
+
return event.composedPath?.()[0] ?? event.target;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Attempts focusing the first element in a list of candidates.
|
|
274
|
+
* Stops when focus has actually moved.
|
|
275
|
+
*
|
|
276
|
+
* Reads the active element from `root` (a focus scope passes its host's `ownerDocument`); falling back to
|
|
277
|
+
* the global `document` would, in an iframe / multi-document scope, never detect that focus moved inside
|
|
278
|
+
* the inner document and so walk past every candidate, leaving focus on the last one instead of the first.
|
|
279
|
+
*/
|
|
280
|
+
function focusFirst(candidates, { select = false, root } = {}) {
|
|
281
|
+
const ownerRoot = root ?? candidates[0]?.ownerDocument ?? document;
|
|
282
|
+
const previouslyFocusedElement = getActiveElement(ownerRoot);
|
|
283
|
+
for (const candidate of candidates) {
|
|
284
|
+
focus(candidate, { select });
|
|
285
|
+
if (getActiveElement(ownerRoot) !== previouslyFocusedElement)
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
157
290
|
function isSelectableInput(element) {
|
|
158
291
|
return element instanceof HTMLInputElement && 'select' in element;
|
|
159
292
|
}
|
|
160
293
|
function focus(element, { select = false } = {}) {
|
|
161
294
|
// only focus if that element is focusable
|
|
162
295
|
if (element && element.focus) {
|
|
163
|
-
|
|
296
|
+
// Read the active element from the element's own document so the same-element select() check stays
|
|
297
|
+
// correct across iframes / multi-document scopes (consistent with this module's ownerDocument focus).
|
|
298
|
+
const root = 'ownerDocument' in element ? element.ownerDocument : undefined;
|
|
299
|
+
const previouslyFocusedElement = getActiveElement(root ?? document);
|
|
164
300
|
// NOTE: we prevent scrolling on focus, to minimize jarring transitions for users
|
|
165
301
|
element.focus({ preventScroll: true });
|
|
166
302
|
// only select if its not the same element, it supports selection and we need to select
|
|
@@ -218,14 +354,17 @@ function createAriaOwnsAnchor(ownerDocument, portalId) {
|
|
|
218
354
|
* trigger steps onto the guard instead of jumping into the content.
|
|
219
355
|
*/
|
|
220
356
|
function disableFocusInside(container) {
|
|
221
|
-
for (const element of
|
|
357
|
+
for (const element of tabbable(container)) {
|
|
222
358
|
element.setAttribute(SAVED_TABINDEX_ATTR, element.getAttribute('tabindex') ?? '');
|
|
223
359
|
element.setAttribute('tabindex', '-1');
|
|
224
360
|
}
|
|
225
361
|
}
|
|
226
|
-
/**
|
|
362
|
+
/**
|
|
363
|
+
* Restores the tabbability that {@link disableFocusInside} suspended. Base UI `enableFocusInside`.
|
|
364
|
+
* Collects markers across the composed tree so guards inside shadow roots / slots are restored too.
|
|
365
|
+
*/
|
|
227
366
|
function enableFocusInside(container) {
|
|
228
|
-
container
|
|
367
|
+
queryComposedAll(container, `[${SAVED_TABINDEX_ATTR}]`).forEach((element) => {
|
|
229
368
|
const original = element.getAttribute(SAVED_TABINDEX_ATTR);
|
|
230
369
|
element.removeAttribute(SAVED_TABINDEX_ATTR);
|
|
231
370
|
if (original) {
|
|
@@ -350,9 +489,6 @@ function arrayRemove(array, item) {
|
|
|
350
489
|
}
|
|
351
490
|
return copy;
|
|
352
491
|
}
|
|
353
|
-
function removeLinks(items) {
|
|
354
|
-
return items.filter((el) => el.tagName !== 'A');
|
|
355
|
-
}
|
|
356
492
|
|
|
357
493
|
const [injectFocusScopeContext, provideFocusScopeContext] = createContext('FocusScope Context', 'utils/focus-scope');
|
|
358
494
|
const rootContext = () => {
|
|
@@ -493,8 +629,9 @@ class RdxFocusScope {
|
|
|
493
629
|
container.addEventListener(AUTOFOCUS_ON_MOUNT, mountEventHandler);
|
|
494
630
|
container.dispatchEvent(mountEvent);
|
|
495
631
|
if (!mountEvent.defaultPrevented) {
|
|
496
|
-
focusFirst(
|
|
497
|
-
select: true
|
|
632
|
+
focusFirst(getTabbableCandidates(container), {
|
|
633
|
+
select: true,
|
|
634
|
+
root: this.ownerDocument
|
|
498
635
|
});
|
|
499
636
|
if (getActiveElement(this.ownerDocument) === previouslyFocusedElement)
|
|
500
637
|
focus(container);
|
|
@@ -546,6 +683,15 @@ class RdxFocusScope {
|
|
|
546
683
|
return (!!active && active !== this.ownerDocument.body && !composedContains(this.elementRef.nativeElement, active));
|
|
547
684
|
}
|
|
548
685
|
handleKeyDown(event) {
|
|
686
|
+
// Only a looping or trapped scope polices Tab at its edges. A plain non-modal scope must let Tab
|
|
687
|
+
// through so the browser can reach whatever follows the host — e.g. the hidden outer focus guards
|
|
688
|
+
// `RdxFloatingFocusManager` places around it (calling `preventDefault()` here would fire first and
|
|
689
|
+
// strand focus at the boundary, defeating the guard). A paused scope (a nested scope took over)
|
|
690
|
+
// also stands down so it can't double-handle a bubbled Tab. Mirrors Base UI / Radix `FocusScope`:
|
|
691
|
+
// `if ((!loop && !trapped) || focusScope.paused) return`.
|
|
692
|
+
if ((!this.loop() && !this.isTrapped()) || this.focusScope.paused()) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
549
695
|
const isTabKey = event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
550
696
|
const focusedElement = getActiveElement(this.ownerDocument);
|
|
551
697
|
if (isTabKey && focusedElement) {
|
|
@@ -592,5 +738,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
|
|
|
592
738
|
* Generated bundle index. Do not edit.
|
|
593
739
|
*/
|
|
594
740
|
|
|
595
|
-
export { AUTOFOCUS_ON_MOUNT, AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS, FOCUS_GUARD_ATTR, FOCUS_GUARD_STYLE, RdxFocusScope, RdxFocusScopeConfigToken, composedContains, createAriaOwnsAnchor, createFocusGuard, disableFocusInside, enableFocusInside,
|
|
741
|
+
export { AUTOFOCUS_ON_MOUNT, AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS, FOCUS_GUARD_ATTR, FOCUS_GUARD_STYLE, RdxFocusScope, RdxFocusScopeConfigToken, composedContains, createAriaOwnsAnchor, createFocusGuard, disableFocusInside, enableFocusInside, focus, focusFirst, focusable, getEventTarget, getNextTabbable, getPreviousTabbable, getTabbableAfterElement, getTabbableBeforeElement, getTabbableCandidates, getTabbableEdges, injectFocusScopeContext, isOutsideEvent, isSelectableInput, isTabbable, provideFocusScopeContext, provideRdxFocusScopeConfig, queryComposedAll, tabbable, useFocusGuardsTabbability };
|
|
596
742
|
//# sourceMappingURL=radix-ng-primitives-focus-scope.mjs.map
|