@primer/behaviors 1.9.0 → 1.9.1-rc.69df32c
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/dist/cjs/anchored-position.js +17 -17
- package/dist/cjs/focus-trap.js +28 -21
- package/dist/cjs/focus-zone.d.ts +1 -0
- package/dist/cjs/focus-zone.js +91 -46
- package/dist/cjs/utils/indexed-set.d.ts +13 -0
- package/dist/cjs/utils/indexed-set.js +53 -0
- package/dist/cjs/utils/iterate-focusable-elements.js +21 -13
- package/dist/esm/anchored-position.mjs +17 -17
- package/dist/esm/focus-trap.mjs +28 -21
- package/dist/esm/focus-zone.d.ts +1 -0
- package/dist/esm/focus-zone.mjs +91 -46
- package/dist/esm/utils/indexed-set.d.ts +13 -0
- package/dist/esm/utils/indexed-set.mjs +51 -0
- package/dist/esm/utils/iterate-focusable-elements.mjs +21 -13
- package/package.json +2 -2
|
@@ -27,9 +27,11 @@ function getPositionedParent(element) {
|
|
|
27
27
|
if (isOnTopLayer(element))
|
|
28
28
|
return document.body;
|
|
29
29
|
let parentNode = element.parentNode;
|
|
30
|
-
while (parentNode !== null) {
|
|
31
|
-
if (parentNode instanceof HTMLElement
|
|
32
|
-
|
|
30
|
+
while (parentNode !== null && parentNode !== document.body) {
|
|
31
|
+
if (parentNode instanceof HTMLElement) {
|
|
32
|
+
if (getComputedStyle(parentNode).position !== 'static') {
|
|
33
|
+
return parentNode;
|
|
34
|
+
}
|
|
33
35
|
}
|
|
34
36
|
parentNode = parentNode.parentNode;
|
|
35
37
|
}
|
|
@@ -51,26 +53,24 @@ function isOnTopLayer(element) {
|
|
|
51
53
|
return false;
|
|
52
54
|
}
|
|
53
55
|
function getClippingRect(element) {
|
|
56
|
+
let clippingNode = document.body;
|
|
54
57
|
let parentNode = element;
|
|
55
|
-
while (parentNode !== null) {
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
while (parentNode !== null && parentNode !== document.body) {
|
|
59
|
+
if (parentNode instanceof HTMLElement) {
|
|
60
|
+
const overflow = getComputedStyle(parentNode).overflow;
|
|
61
|
+
if (overflow !== 'visible') {
|
|
62
|
+
clippingNode = parentNode;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
62
65
|
}
|
|
63
66
|
parentNode = parentNode.parentNode;
|
|
64
67
|
}
|
|
65
|
-
const clippingNode = parentNode === document.body || !(parentNode instanceof HTMLElement) ? document.body : parentNode;
|
|
66
68
|
const elemRect = clippingNode.getBoundingClientRect();
|
|
67
69
|
const elemStyle = getComputedStyle(clippingNode);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
elemStyle.borderBottomWidth,
|
|
73
|
-
].map(v => parseInt(v, 10) || 0);
|
|
70
|
+
const borderTop = parseInt(elemStyle.borderTopWidth, 10) || 0;
|
|
71
|
+
const borderLeft = parseInt(elemStyle.borderLeftWidth, 10) || 0;
|
|
72
|
+
const borderRight = parseInt(elemStyle.borderRightWidth, 10) || 0;
|
|
73
|
+
const borderBottom = parseInt(elemStyle.borderBottomWidth, 10) || 0;
|
|
74
74
|
return {
|
|
75
75
|
top: elemRect.top + borderTop,
|
|
76
76
|
left: elemRect.left + borderLeft,
|
package/dist/cjs/focus-trap.js
CHANGED
|
@@ -6,6 +6,16 @@ var eventListenerSignal = require('./polyfills/event-listener-signal.js');
|
|
|
6
6
|
eventListenerSignal.polyfill();
|
|
7
7
|
const suspendedTrapStack = [];
|
|
8
8
|
let activeTrap = undefined;
|
|
9
|
+
const SR_ONLY_STYLES = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0';
|
|
10
|
+
function createSentinel({ onFocus }) {
|
|
11
|
+
const sentinel = document.createElement('span');
|
|
12
|
+
sentinel.setAttribute('class', 'sentinel');
|
|
13
|
+
sentinel.setAttribute('tabindex', '0');
|
|
14
|
+
sentinel.setAttribute('aria-hidden', 'true');
|
|
15
|
+
sentinel.style.cssText = SR_ONLY_STYLES;
|
|
16
|
+
sentinel.onfocus = onFocus;
|
|
17
|
+
return sentinel;
|
|
18
|
+
}
|
|
9
19
|
function tryReactivate() {
|
|
10
20
|
const trapToReactivate = suspendedTrapStack.pop();
|
|
11
21
|
if (trapToReactivate) {
|
|
@@ -23,9 +33,10 @@ function observeFocusTrap(container, sentinels) {
|
|
|
23
33
|
const observer = new MutationObserver(mutations => {
|
|
24
34
|
for (const mutation of mutations) {
|
|
25
35
|
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
for (const node of mutation.addedNodes) {
|
|
37
|
+
if (node instanceof HTMLElement && node.tagName === 'SPAN' && node.classList.contains('sentinel')) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
29
40
|
}
|
|
30
41
|
const firstChild = container.firstElementChild;
|
|
31
42
|
const lastChild = container.lastElementChild;
|
|
@@ -46,24 +57,20 @@ function focusTrap(container, initialFocus, abortSignal) {
|
|
|
46
57
|
const controller = new AbortController();
|
|
47
58
|
const signal = abortSignal !== null && abortSignal !== void 0 ? abortSignal : controller.signal;
|
|
48
59
|
container.setAttribute('data-focus-trap', 'active');
|
|
49
|
-
const sentinelStart =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
firstFocusableChild === null || firstFocusableChild === void 0 ? void 0 : firstFocusableChild.focus();
|
|
64
|
-
};
|
|
65
|
-
const existingSentinels = Array.from(container.children).filter(e => e.classList.contains('sentinel') && e.tagName === 'SPAN');
|
|
66
|
-
if (!existingSentinels.length) {
|
|
60
|
+
const sentinelStart = createSentinel({
|
|
61
|
+
onFocus: () => {
|
|
62
|
+
const lastFocusableChild = iterateFocusableElements.getFocusableChild(container, true);
|
|
63
|
+
lastFocusableChild === null || lastFocusableChild === void 0 ? void 0 : lastFocusableChild.focus();
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const sentinelEnd = createSentinel({
|
|
67
|
+
onFocus: () => {
|
|
68
|
+
const firstFocusableChild = iterateFocusableElements.getFocusableChild(container);
|
|
69
|
+
firstFocusableChild === null || firstFocusableChild === void 0 ? void 0 : firstFocusableChild.focus();
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const hasExistingSentinels = container.querySelector(':scope > span.sentinel') !== null;
|
|
73
|
+
if (!hasExistingSentinels) {
|
|
67
74
|
container.prepend(sentinelStart);
|
|
68
75
|
container.append(sentinelEnd);
|
|
69
76
|
}
|
package/dist/cjs/focus-zone.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export type FocusZoneSettings = IterateFocusableElements & {
|
|
|
28
28
|
focusInStrategy?: 'first' | 'closest' | 'previous' | 'initial' | ((previousFocusedElement: Element) => HTMLElement | undefined);
|
|
29
29
|
preventScroll?: boolean;
|
|
30
30
|
ignoreHoverEvents?: boolean;
|
|
31
|
+
focusPrependedElements?: boolean;
|
|
31
32
|
};
|
|
32
33
|
export declare const isActiveDescendantAttribute = "data-is-active-descendant";
|
|
33
34
|
export declare const activeDescendantActivatedDirectly = "activated-directly";
|
package/dist/cjs/focus-zone.js
CHANGED
|
@@ -5,6 +5,7 @@ var userAgent = require('./utils/user-agent.js');
|
|
|
5
5
|
var iterateFocusableElements = require('./utils/iterate-focusable-elements.js');
|
|
6
6
|
var uniqueId = require('./utils/unique-id.js');
|
|
7
7
|
var isEditableElement = require('./utils/is-editable-element.js');
|
|
8
|
+
var indexedSet = require('./utils/indexed-set.js');
|
|
8
9
|
|
|
9
10
|
eventListenerSignal.polyfill();
|
|
10
11
|
exports.FocusKeys = void 0;
|
|
@@ -82,17 +83,18 @@ function getDirection(keyboardEvent) {
|
|
|
82
83
|
}
|
|
83
84
|
function shouldIgnoreFocusHandling(keyboardEvent, activeElement) {
|
|
84
85
|
const key = keyboardEvent.key;
|
|
85
|
-
const
|
|
86
|
+
const isSingleChar = key.length === 1 || (key.length === 2 && key.charCodeAt(0) >= 0xd800 && key.charCodeAt(0) <= 0xdbff);
|
|
86
87
|
const isEditable = isEditableElement.isEditableElement(activeElement);
|
|
87
88
|
const isSelect = activeElement instanceof HTMLSelectElement;
|
|
88
|
-
if (isEditable && (
|
|
89
|
+
if (isEditable && (isSingleChar || key === 'Home' || key === 'End')) {
|
|
89
90
|
return true;
|
|
90
91
|
}
|
|
91
92
|
if (isSelect) {
|
|
92
|
-
|
|
93
|
+
const isMac = userAgent.isMacOS();
|
|
94
|
+
if (key === 'ArrowDown' && isMac && !keyboardEvent.metaKey) {
|
|
93
95
|
return true;
|
|
94
96
|
}
|
|
95
|
-
if (key === 'ArrowDown' && !
|
|
97
|
+
if (key === 'ArrowDown' && !isMac && keyboardEvent.altKey) {
|
|
96
98
|
return true;
|
|
97
99
|
}
|
|
98
100
|
return false;
|
|
@@ -129,8 +131,8 @@ const activeDescendantActivatedDirectly = 'activated-directly';
|
|
|
129
131
|
const activeDescendantActivatedIndirectly = 'activated-indirectly';
|
|
130
132
|
const hasActiveDescendantAttribute = 'data-has-active-descendant';
|
|
131
133
|
function focusZone(container, settings) {
|
|
132
|
-
var _a, _b, _c, _d, _e, _f;
|
|
133
|
-
const focusableElements =
|
|
134
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
135
|
+
const focusableElements = new indexedSet.IndexedSet();
|
|
134
136
|
const savedTabIndex = new WeakMap();
|
|
135
137
|
const bindKeys = (_a = settings === null || settings === void 0 ? void 0 : settings.bindKeys) !== null && _a !== void 0 ? _a : ((settings === null || settings === void 0 ? void 0 : settings.getNextFocusable) ? exports.FocusKeys.ArrowAll : exports.FocusKeys.ArrowVertical) | exports.FocusKeys.HomeAndEnd;
|
|
136
138
|
const focusOutBehavior = (_b = settings === null || settings === void 0 ? void 0 : settings.focusOutBehavior) !== null && _b !== void 0 ? _b : 'stop';
|
|
@@ -138,11 +140,13 @@ function focusZone(container, settings) {
|
|
|
138
140
|
const activeDescendantControl = settings === null || settings === void 0 ? void 0 : settings.activeDescendantControl;
|
|
139
141
|
const activeDescendantCallback = settings === null || settings === void 0 ? void 0 : settings.onActiveDescendantChanged;
|
|
140
142
|
const ignoreHoverEvents = (_d = settings === null || settings === void 0 ? void 0 : settings.ignoreHoverEvents) !== null && _d !== void 0 ? _d : false;
|
|
143
|
+
const focusPrependedElements = (_e = settings === null || settings === void 0 ? void 0 : settings.focusPrependedElements) !== null && _e !== void 0 ? _e : false;
|
|
141
144
|
let currentFocusedElement;
|
|
142
|
-
|
|
145
|
+
let wasDirectlyActivated = false;
|
|
146
|
+
const preventScroll = (_f = settings === null || settings === void 0 ? void 0 : settings.preventScroll) !== null && _f !== void 0 ? _f : false;
|
|
143
147
|
const preventInitialFocus = focusInStrategy === 'initial' && (settings === null || settings === void 0 ? void 0 : settings.activeDescendantControl);
|
|
144
148
|
function getFirstFocusableElement() {
|
|
145
|
-
return focusableElements
|
|
149
|
+
return focusableElements.get(0);
|
|
146
150
|
}
|
|
147
151
|
function isActiveDescendantInputFocused() {
|
|
148
152
|
return document.activeElement === activeDescendantControl;
|
|
@@ -150,6 +154,7 @@ function focusZone(container, settings) {
|
|
|
150
154
|
function updateFocusedElement(to, directlyActivated = false) {
|
|
151
155
|
const from = currentFocusedElement;
|
|
152
156
|
currentFocusedElement = to;
|
|
157
|
+
wasDirectlyActivated = directlyActivated;
|
|
153
158
|
if (activeDescendantControl) {
|
|
154
159
|
if (to && isActiveDescendantInputFocused()) {
|
|
155
160
|
setActiveDescendant(from, to, directlyActivated);
|
|
@@ -187,36 +192,41 @@ function focusZone(container, settings) {
|
|
|
187
192
|
activeDescendantControl === null || activeDescendantControl === void 0 ? void 0 : activeDescendantControl.removeAttribute('aria-activedescendant');
|
|
188
193
|
container.removeAttribute(hasActiveDescendantAttribute);
|
|
189
194
|
previouslyActiveElement === null || previouslyActiveElement === void 0 ? void 0 : previouslyActiveElement.removeAttribute(isActiveDescendantAttribute);
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
const items = container.querySelectorAll(`[${isActiveDescendantAttribute}]`);
|
|
196
|
+
for (let i = 0; i < items.length; i++) {
|
|
197
|
+
items[i].removeAttribute(isActiveDescendantAttribute);
|
|
192
198
|
}
|
|
193
199
|
activeDescendantCallback === null || activeDescendantCallback === void 0 ? void 0 : activeDescendantCallback(undefined, previouslyActiveElement, false);
|
|
194
200
|
}
|
|
195
201
|
function beginFocusManagement(...elements) {
|
|
196
|
-
const filteredElements =
|
|
202
|
+
const filteredElements = (settings === null || settings === void 0 ? void 0 : settings.focusableElementFilter)
|
|
203
|
+
? elements.filter(e => settings.focusableElementFilter(e))
|
|
204
|
+
: elements;
|
|
197
205
|
if (filteredElements.length === 0) {
|
|
198
206
|
return;
|
|
199
207
|
}
|
|
200
|
-
|
|
208
|
+
const insertionIndex = findInsertionIndex(filteredElements);
|
|
209
|
+
focusableElements.insertAt(insertionIndex, ...filteredElements);
|
|
201
210
|
for (const element of filteredElements) {
|
|
202
211
|
if (!savedTabIndex.has(element)) {
|
|
203
212
|
savedTabIndex.set(element, element.getAttribute('tabindex'));
|
|
204
213
|
}
|
|
205
214
|
element.setAttribute('tabindex', '-1');
|
|
206
215
|
}
|
|
207
|
-
|
|
216
|
+
const shouldFocusPrepended = focusPrependedElements && insertionIndex === 0 && !wasDirectlyActivated;
|
|
217
|
+
if (!preventInitialFocus && (!currentFocusedElement || shouldFocusPrepended)) {
|
|
208
218
|
updateFocusedElement(getFirstFocusableElement());
|
|
209
219
|
}
|
|
210
220
|
}
|
|
211
221
|
function findInsertionIndex(elementsToInsert) {
|
|
212
222
|
const firstElementToInsert = elementsToInsert[0];
|
|
213
|
-
if (focusableElements.
|
|
223
|
+
if (focusableElements.size === 0)
|
|
214
224
|
return 0;
|
|
215
225
|
let iMin = 0;
|
|
216
|
-
let iMax = focusableElements.
|
|
226
|
+
let iMax = focusableElements.size - 1;
|
|
217
227
|
while (iMin <= iMax) {
|
|
218
228
|
const i = Math.floor((iMin + iMax) / 2);
|
|
219
|
-
const element = focusableElements
|
|
229
|
+
const element = focusableElements.get(i);
|
|
220
230
|
if (followsInDocument(firstElementToInsert, element)) {
|
|
221
231
|
iMax = i - 1;
|
|
222
232
|
}
|
|
@@ -231,10 +241,7 @@ function focusZone(container, settings) {
|
|
|
231
241
|
}
|
|
232
242
|
function endFocusManagement(...elements) {
|
|
233
243
|
for (const element of elements) {
|
|
234
|
-
|
|
235
|
-
if (focusableElementIndex >= 0) {
|
|
236
|
-
focusableElements.splice(focusableElementIndex, 1);
|
|
237
|
-
}
|
|
244
|
+
focusableElements.delete(element);
|
|
238
245
|
const savedIndex = savedTabIndex.get(element);
|
|
239
246
|
if (savedIndex !== undefined) {
|
|
240
247
|
if (savedIndex === null) {
|
|
@@ -261,29 +268,61 @@ function focusZone(container, settings) {
|
|
|
261
268
|
if (!preventInitialFocus)
|
|
262
269
|
updateFocusedElement(initialElement);
|
|
263
270
|
const observer = new MutationObserver(mutations => {
|
|
271
|
+
const elementsToRemove = new Set();
|
|
272
|
+
const elementsToAdd = new Set();
|
|
273
|
+
const attributeRemovals = new Set();
|
|
274
|
+
const attributeAdditions = new Set();
|
|
264
275
|
for (const mutation of mutations) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
if (mutation.type === 'childList') {
|
|
277
|
+
for (const removedNode of mutation.removedNodes) {
|
|
278
|
+
if (removedNode instanceof HTMLElement) {
|
|
279
|
+
elementsToRemove.add(removedNode);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const addedNode of mutation.addedNodes) {
|
|
283
|
+
if (addedNode instanceof HTMLElement) {
|
|
284
|
+
elementsToAdd.add(addedNode);
|
|
285
|
+
}
|
|
268
286
|
}
|
|
269
287
|
}
|
|
270
|
-
if (mutation.type === 'attributes' && mutation.
|
|
271
|
-
|
|
272
|
-
|
|
288
|
+
else if (mutation.type === 'attributes' && mutation.target instanceof HTMLElement) {
|
|
289
|
+
const attributeName = mutation.attributeName;
|
|
290
|
+
const hasAttribute = attributeName ? mutation.target.hasAttribute(attributeName) : false;
|
|
291
|
+
if (hasAttribute) {
|
|
292
|
+
attributeRemovals.add(mutation.target);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
attributeAdditions.add(mutation.target);
|
|
273
296
|
}
|
|
274
297
|
}
|
|
275
298
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
299
|
+
if (elementsToRemove.size > 0) {
|
|
300
|
+
const toRemove = [];
|
|
301
|
+
for (const node of elementsToRemove) {
|
|
302
|
+
for (const el of iterateFocusableElements.iterateFocusableElements(node)) {
|
|
303
|
+
toRemove.push(el);
|
|
280
304
|
}
|
|
281
305
|
}
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
306
|
+
if (toRemove.length > 0) {
|
|
307
|
+
endFocusManagement(...toRemove);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (attributeRemovals.size > 0) {
|
|
311
|
+
endFocusManagement(...attributeRemovals);
|
|
312
|
+
}
|
|
313
|
+
if (elementsToAdd.size > 0) {
|
|
314
|
+
const toAdd = [];
|
|
315
|
+
for (const node of elementsToAdd) {
|
|
316
|
+
for (const el of iterateFocusableElements.iterateFocusableElements(node, iterateFocusableElementsOptions)) {
|
|
317
|
+
toAdd.push(el);
|
|
285
318
|
}
|
|
286
319
|
}
|
|
320
|
+
if (toAdd.length > 0) {
|
|
321
|
+
beginFocusManagement(...toAdd);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (attributeAdditions.size > 0) {
|
|
325
|
+
beginFocusManagement(...attributeAdditions);
|
|
287
326
|
}
|
|
288
327
|
});
|
|
289
328
|
observer.observe(container, {
|
|
@@ -293,9 +332,11 @@ function focusZone(container, settings) {
|
|
|
293
332
|
attributeOldValue: true,
|
|
294
333
|
});
|
|
295
334
|
const controller = new AbortController();
|
|
296
|
-
const signal = (
|
|
335
|
+
const signal = (_g = settings === null || settings === void 0 ? void 0 : settings.abortSignal) !== null && _g !== void 0 ? _g : controller.signal;
|
|
297
336
|
signal.addEventListener('abort', () => {
|
|
337
|
+
observer.disconnect();
|
|
298
338
|
endFocusManagement(...focusableElements);
|
|
339
|
+
focusableElements.clear();
|
|
299
340
|
});
|
|
300
341
|
let elementIndexFocusedByClick = undefined;
|
|
301
342
|
container.addEventListener('mousedown', event => {
|
|
@@ -305,7 +346,7 @@ function focusZone(container, settings) {
|
|
|
305
346
|
}, { signal });
|
|
306
347
|
if (activeDescendantControl) {
|
|
307
348
|
container.addEventListener('focusin', event => {
|
|
308
|
-
if (event.target instanceof HTMLElement && focusableElements.
|
|
349
|
+
if (event.target instanceof HTMLElement && focusableElements.has(event.target)) {
|
|
309
350
|
activeDescendantControl.focus({ preventScroll });
|
|
310
351
|
updateFocusedElement(event.target);
|
|
311
352
|
}
|
|
@@ -315,6 +356,10 @@ function focusZone(container, settings) {
|
|
|
315
356
|
if (!(target instanceof Node)) {
|
|
316
357
|
return;
|
|
317
358
|
}
|
|
359
|
+
if (target instanceof HTMLElement && focusableElements.has(target)) {
|
|
360
|
+
updateFocusedElement(target);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
318
363
|
const focusableElement = focusableElements.find(element => element.contains(target));
|
|
319
364
|
if (focusableElement) {
|
|
320
365
|
updateFocusedElement(focusableElement);
|
|
@@ -339,8 +384,9 @@ function focusZone(container, settings) {
|
|
|
339
384
|
if (event.target instanceof HTMLElement) {
|
|
340
385
|
if (elementIndexFocusedByClick !== undefined) {
|
|
341
386
|
if (elementIndexFocusedByClick >= 0) {
|
|
342
|
-
|
|
343
|
-
|
|
387
|
+
const clickedElement = focusableElements.get(elementIndexFocusedByClick);
|
|
388
|
+
if (clickedElement && clickedElement !== currentFocusedElement) {
|
|
389
|
+
updateFocusedElement(clickedElement);
|
|
344
390
|
}
|
|
345
391
|
}
|
|
346
392
|
elementIndexFocusedByClick = undefined;
|
|
@@ -351,8 +397,8 @@ function focusZone(container, settings) {
|
|
|
351
397
|
}
|
|
352
398
|
else if (focusInStrategy === 'closest' || focusInStrategy === 'first') {
|
|
353
399
|
if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
|
|
354
|
-
const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.
|
|
355
|
-
const targetElement = focusableElements
|
|
400
|
+
const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.size - 1 : 0;
|
|
401
|
+
const targetElement = focusableElements.get(targetElementIndex);
|
|
356
402
|
targetElement === null || targetElement === void 0 ? void 0 : targetElement.focus({ preventScroll });
|
|
357
403
|
return;
|
|
358
404
|
}
|
|
@@ -363,8 +409,7 @@ function focusZone(container, settings) {
|
|
|
363
409
|
else if (typeof focusInStrategy === 'function') {
|
|
364
410
|
if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
|
|
365
411
|
const elementToFocus = focusInStrategy(event.relatedTarget);
|
|
366
|
-
|
|
367
|
-
if (requestedFocusElementIndex >= 0 && elementToFocus instanceof HTMLElement) {
|
|
412
|
+
if (elementToFocus && focusableElements.has(elementToFocus)) {
|
|
368
413
|
elementToFocus.focus({ preventScroll });
|
|
369
414
|
return;
|
|
370
415
|
}
|
|
@@ -423,26 +468,26 @@ function focusZone(container, settings) {
|
|
|
423
468
|
nextFocusedIndex += 1;
|
|
424
469
|
}
|
|
425
470
|
else {
|
|
426
|
-
nextFocusedIndex = focusableElements.
|
|
471
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
427
472
|
}
|
|
428
473
|
if (nextFocusedIndex < 0) {
|
|
429
474
|
if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
|
|
430
|
-
nextFocusedIndex = focusableElements.
|
|
475
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
431
476
|
}
|
|
432
477
|
else {
|
|
433
478
|
nextFocusedIndex = 0;
|
|
434
479
|
}
|
|
435
480
|
}
|
|
436
|
-
if (nextFocusedIndex >= focusableElements.
|
|
481
|
+
if (nextFocusedIndex >= focusableElements.size) {
|
|
437
482
|
if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
|
|
438
483
|
nextFocusedIndex = 0;
|
|
439
484
|
}
|
|
440
485
|
else {
|
|
441
|
-
nextFocusedIndex = focusableElements.
|
|
486
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
442
487
|
}
|
|
443
488
|
}
|
|
444
489
|
if (lastFocusedIndex !== nextFocusedIndex) {
|
|
445
|
-
nextElementToFocus = focusableElements
|
|
490
|
+
nextElementToFocus = focusableElements.get(nextFocusedIndex);
|
|
446
491
|
}
|
|
447
492
|
}
|
|
448
493
|
if (activeDescendantControl) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class IndexedSet<T> {
|
|
2
|
+
private _items;
|
|
3
|
+
private _itemSet;
|
|
4
|
+
insertAt(index: number, ...elements: T[]): void;
|
|
5
|
+
delete(element: T): boolean;
|
|
6
|
+
has(element: T): boolean;
|
|
7
|
+
indexOf(element: T): number;
|
|
8
|
+
get(index: number): T | undefined;
|
|
9
|
+
get size(): number;
|
|
10
|
+
[Symbol.iterator](): Iterator<T>;
|
|
11
|
+
clear(): void;
|
|
12
|
+
find(predicate: (element: T) => boolean): T | undefined;
|
|
13
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class IndexedSet {
|
|
4
|
+
constructor() {
|
|
5
|
+
this._items = [];
|
|
6
|
+
this._itemSet = new Set();
|
|
7
|
+
}
|
|
8
|
+
insertAt(index, ...elements) {
|
|
9
|
+
const newElements = elements.filter(e => !this._itemSet.has(e));
|
|
10
|
+
if (newElements.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
this._items.splice(index, 0, ...newElements);
|
|
13
|
+
for (const element of newElements) {
|
|
14
|
+
this._itemSet.add(element);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
delete(element) {
|
|
18
|
+
if (!this._itemSet.has(element))
|
|
19
|
+
return false;
|
|
20
|
+
const index = this._items.indexOf(element);
|
|
21
|
+
if (index >= 0) {
|
|
22
|
+
this._items.splice(index, 1);
|
|
23
|
+
}
|
|
24
|
+
this._itemSet.delete(element);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
has(element) {
|
|
28
|
+
return this._itemSet.has(element);
|
|
29
|
+
}
|
|
30
|
+
indexOf(element) {
|
|
31
|
+
if (!this._itemSet.has(element))
|
|
32
|
+
return -1;
|
|
33
|
+
return this._items.indexOf(element);
|
|
34
|
+
}
|
|
35
|
+
get(index) {
|
|
36
|
+
return this._items[index];
|
|
37
|
+
}
|
|
38
|
+
get size() {
|
|
39
|
+
return this._items.length;
|
|
40
|
+
}
|
|
41
|
+
[Symbol.iterator]() {
|
|
42
|
+
return this._items[Symbol.iterator]();
|
|
43
|
+
}
|
|
44
|
+
clear() {
|
|
45
|
+
this._items = [];
|
|
46
|
+
this._itemSet.clear();
|
|
47
|
+
}
|
|
48
|
+
find(predicate) {
|
|
49
|
+
return this._items.find(predicate);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
exports.IndexedSet = IndexedSet;
|
|
@@ -33,24 +33,32 @@ function* iterateFocusableElements(container, options = {}) {
|
|
|
33
33
|
function getFocusableChild(container, lastChild = false) {
|
|
34
34
|
return iterateFocusableElements(container, { reverse: lastChild, strict: true, onlyTabbable: true }).next().value;
|
|
35
35
|
}
|
|
36
|
+
const DISABLEABLE_TAGS = new Set(['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTGROUP', 'OPTION', 'FIELDSET']);
|
|
36
37
|
function isFocusable(elem, strict = false) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
if (elem.hidden)
|
|
39
|
+
return false;
|
|
40
|
+
if (elem.classList.contains('sentinel'))
|
|
41
|
+
return false;
|
|
42
|
+
if (elem instanceof HTMLInputElement && elem.type === 'hidden')
|
|
43
|
+
return false;
|
|
44
|
+
if (DISABLEABLE_TAGS.has(elem.tagName) && elem.disabled)
|
|
43
45
|
return false;
|
|
44
|
-
}
|
|
45
46
|
if (strict) {
|
|
47
|
+
const offsetWidth = elem.offsetWidth;
|
|
48
|
+
const offsetHeight = elem.offsetHeight;
|
|
49
|
+
const offsetParent = elem.offsetParent;
|
|
50
|
+
if (offsetWidth === 0 || offsetHeight === 0)
|
|
51
|
+
return false;
|
|
46
52
|
const style = getComputedStyle(elem);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
if (style.display === 'none')
|
|
54
|
+
return false;
|
|
55
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse')
|
|
56
|
+
return false;
|
|
57
|
+
const position = style.position;
|
|
58
|
+
if (!offsetParent && position !== 'fixed' && position !== 'sticky')
|
|
59
|
+
return false;
|
|
60
|
+
if (elem.getClientRects().length === 0)
|
|
52
61
|
return false;
|
|
53
|
-
}
|
|
54
62
|
}
|
|
55
63
|
if (elem.getAttribute('tabindex') != null) {
|
|
56
64
|
return true;
|
|
@@ -25,9 +25,11 @@ function getPositionedParent(element) {
|
|
|
25
25
|
if (isOnTopLayer(element))
|
|
26
26
|
return document.body;
|
|
27
27
|
let parentNode = element.parentNode;
|
|
28
|
-
while (parentNode !== null) {
|
|
29
|
-
if (parentNode instanceof HTMLElement
|
|
30
|
-
|
|
28
|
+
while (parentNode !== null && parentNode !== document.body) {
|
|
29
|
+
if (parentNode instanceof HTMLElement) {
|
|
30
|
+
if (getComputedStyle(parentNode).position !== 'static') {
|
|
31
|
+
return parentNode;
|
|
32
|
+
}
|
|
31
33
|
}
|
|
32
34
|
parentNode = parentNode.parentNode;
|
|
33
35
|
}
|
|
@@ -49,26 +51,24 @@ function isOnTopLayer(element) {
|
|
|
49
51
|
return false;
|
|
50
52
|
}
|
|
51
53
|
function getClippingRect(element) {
|
|
54
|
+
let clippingNode = document.body;
|
|
52
55
|
let parentNode = element;
|
|
53
|
-
while (parentNode !== null) {
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
while (parentNode !== null && parentNode !== document.body) {
|
|
57
|
+
if (parentNode instanceof HTMLElement) {
|
|
58
|
+
const overflow = getComputedStyle(parentNode).overflow;
|
|
59
|
+
if (overflow !== 'visible') {
|
|
60
|
+
clippingNode = parentNode;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
60
63
|
}
|
|
61
64
|
parentNode = parentNode.parentNode;
|
|
62
65
|
}
|
|
63
|
-
const clippingNode = parentNode === document.body || !(parentNode instanceof HTMLElement) ? document.body : parentNode;
|
|
64
66
|
const elemRect = clippingNode.getBoundingClientRect();
|
|
65
67
|
const elemStyle = getComputedStyle(clippingNode);
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
elemStyle.borderBottomWidth,
|
|
71
|
-
].map(v => parseInt(v, 10) || 0);
|
|
68
|
+
const borderTop = parseInt(elemStyle.borderTopWidth, 10) || 0;
|
|
69
|
+
const borderLeft = parseInt(elemStyle.borderLeftWidth, 10) || 0;
|
|
70
|
+
const borderRight = parseInt(elemStyle.borderRightWidth, 10) || 0;
|
|
71
|
+
const borderBottom = parseInt(elemStyle.borderBottomWidth, 10) || 0;
|
|
72
72
|
return {
|
|
73
73
|
top: elemRect.top + borderTop,
|
|
74
74
|
left: elemRect.left + borderLeft,
|
package/dist/esm/focus-trap.mjs
CHANGED
|
@@ -4,6 +4,16 @@ import { polyfill } from './polyfills/event-listener-signal.mjs';
|
|
|
4
4
|
polyfill();
|
|
5
5
|
const suspendedTrapStack = [];
|
|
6
6
|
let activeTrap = undefined;
|
|
7
|
+
const SR_ONLY_STYLES = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0';
|
|
8
|
+
function createSentinel({ onFocus }) {
|
|
9
|
+
const sentinel = document.createElement('span');
|
|
10
|
+
sentinel.setAttribute('class', 'sentinel');
|
|
11
|
+
sentinel.setAttribute('tabindex', '0');
|
|
12
|
+
sentinel.setAttribute('aria-hidden', 'true');
|
|
13
|
+
sentinel.style.cssText = SR_ONLY_STYLES;
|
|
14
|
+
sentinel.onfocus = onFocus;
|
|
15
|
+
return sentinel;
|
|
16
|
+
}
|
|
7
17
|
function tryReactivate() {
|
|
8
18
|
const trapToReactivate = suspendedTrapStack.pop();
|
|
9
19
|
if (trapToReactivate) {
|
|
@@ -21,9 +31,10 @@ function observeFocusTrap(container, sentinels) {
|
|
|
21
31
|
const observer = new MutationObserver(mutations => {
|
|
22
32
|
for (const mutation of mutations) {
|
|
23
33
|
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
for (const node of mutation.addedNodes) {
|
|
35
|
+
if (node instanceof HTMLElement && node.tagName === 'SPAN' && node.classList.contains('sentinel')) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
27
38
|
}
|
|
28
39
|
const firstChild = container.firstElementChild;
|
|
29
40
|
const lastChild = container.lastElementChild;
|
|
@@ -44,24 +55,20 @@ function focusTrap(container, initialFocus, abortSignal) {
|
|
|
44
55
|
const controller = new AbortController();
|
|
45
56
|
const signal = abortSignal !== null && abortSignal !== void 0 ? abortSignal : controller.signal;
|
|
46
57
|
container.setAttribute('data-focus-trap', 'active');
|
|
47
|
-
const sentinelStart =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
firstFocusableChild === null || firstFocusableChild === void 0 ? void 0 : firstFocusableChild.focus();
|
|
62
|
-
};
|
|
63
|
-
const existingSentinels = Array.from(container.children).filter(e => e.classList.contains('sentinel') && e.tagName === 'SPAN');
|
|
64
|
-
if (!existingSentinels.length) {
|
|
58
|
+
const sentinelStart = createSentinel({
|
|
59
|
+
onFocus: () => {
|
|
60
|
+
const lastFocusableChild = getFocusableChild(container, true);
|
|
61
|
+
lastFocusableChild === null || lastFocusableChild === void 0 ? void 0 : lastFocusableChild.focus();
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const sentinelEnd = createSentinel({
|
|
65
|
+
onFocus: () => {
|
|
66
|
+
const firstFocusableChild = getFocusableChild(container);
|
|
67
|
+
firstFocusableChild === null || firstFocusableChild === void 0 ? void 0 : firstFocusableChild.focus();
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const hasExistingSentinels = container.querySelector(':scope > span.sentinel') !== null;
|
|
71
|
+
if (!hasExistingSentinels) {
|
|
65
72
|
container.prepend(sentinelStart);
|
|
66
73
|
container.append(sentinelEnd);
|
|
67
74
|
}
|
package/dist/esm/focus-zone.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export type FocusZoneSettings = IterateFocusableElements & {
|
|
|
28
28
|
focusInStrategy?: 'first' | 'closest' | 'previous' | 'initial' | ((previousFocusedElement: Element) => HTMLElement | undefined);
|
|
29
29
|
preventScroll?: boolean;
|
|
30
30
|
ignoreHoverEvents?: boolean;
|
|
31
|
+
focusPrependedElements?: boolean;
|
|
31
32
|
};
|
|
32
33
|
export declare const isActiveDescendantAttribute = "data-is-active-descendant";
|
|
33
34
|
export declare const activeDescendantActivatedDirectly = "activated-directly";
|
package/dist/esm/focus-zone.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { isMacOS } from './utils/user-agent.mjs';
|
|
|
3
3
|
import { iterateFocusableElements } from './utils/iterate-focusable-elements.mjs';
|
|
4
4
|
import { uniqueId } from './utils/unique-id.mjs';
|
|
5
5
|
import { isEditableElement } from './utils/is-editable-element.mjs';
|
|
6
|
+
import { IndexedSet } from './utils/indexed-set.mjs';
|
|
6
7
|
|
|
7
8
|
polyfill();
|
|
8
9
|
var FocusKeys;
|
|
@@ -80,17 +81,18 @@ function getDirection(keyboardEvent) {
|
|
|
80
81
|
}
|
|
81
82
|
function shouldIgnoreFocusHandling(keyboardEvent, activeElement) {
|
|
82
83
|
const key = keyboardEvent.key;
|
|
83
|
-
const
|
|
84
|
+
const isSingleChar = key.length === 1 || (key.length === 2 && key.charCodeAt(0) >= 0xd800 && key.charCodeAt(0) <= 0xdbff);
|
|
84
85
|
const isEditable = isEditableElement(activeElement);
|
|
85
86
|
const isSelect = activeElement instanceof HTMLSelectElement;
|
|
86
|
-
if (isEditable && (
|
|
87
|
+
if (isEditable && (isSingleChar || key === 'Home' || key === 'End')) {
|
|
87
88
|
return true;
|
|
88
89
|
}
|
|
89
90
|
if (isSelect) {
|
|
90
|
-
|
|
91
|
+
const isMac = isMacOS();
|
|
92
|
+
if (key === 'ArrowDown' && isMac && !keyboardEvent.metaKey) {
|
|
91
93
|
return true;
|
|
92
94
|
}
|
|
93
|
-
if (key === 'ArrowDown' && !
|
|
95
|
+
if (key === 'ArrowDown' && !isMac && keyboardEvent.altKey) {
|
|
94
96
|
return true;
|
|
95
97
|
}
|
|
96
98
|
return false;
|
|
@@ -127,8 +129,8 @@ const activeDescendantActivatedDirectly = 'activated-directly';
|
|
|
127
129
|
const activeDescendantActivatedIndirectly = 'activated-indirectly';
|
|
128
130
|
const hasActiveDescendantAttribute = 'data-has-active-descendant';
|
|
129
131
|
function focusZone(container, settings) {
|
|
130
|
-
var _a, _b, _c, _d, _e, _f;
|
|
131
|
-
const focusableElements =
|
|
132
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
133
|
+
const focusableElements = new IndexedSet();
|
|
132
134
|
const savedTabIndex = new WeakMap();
|
|
133
135
|
const bindKeys = (_a = settings === null || settings === void 0 ? void 0 : settings.bindKeys) !== null && _a !== void 0 ? _a : ((settings === null || settings === void 0 ? void 0 : settings.getNextFocusable) ? FocusKeys.ArrowAll : FocusKeys.ArrowVertical) | FocusKeys.HomeAndEnd;
|
|
134
136
|
const focusOutBehavior = (_b = settings === null || settings === void 0 ? void 0 : settings.focusOutBehavior) !== null && _b !== void 0 ? _b : 'stop';
|
|
@@ -136,11 +138,13 @@ function focusZone(container, settings) {
|
|
|
136
138
|
const activeDescendantControl = settings === null || settings === void 0 ? void 0 : settings.activeDescendantControl;
|
|
137
139
|
const activeDescendantCallback = settings === null || settings === void 0 ? void 0 : settings.onActiveDescendantChanged;
|
|
138
140
|
const ignoreHoverEvents = (_d = settings === null || settings === void 0 ? void 0 : settings.ignoreHoverEvents) !== null && _d !== void 0 ? _d : false;
|
|
141
|
+
const focusPrependedElements = (_e = settings === null || settings === void 0 ? void 0 : settings.focusPrependedElements) !== null && _e !== void 0 ? _e : false;
|
|
139
142
|
let currentFocusedElement;
|
|
140
|
-
|
|
143
|
+
let wasDirectlyActivated = false;
|
|
144
|
+
const preventScroll = (_f = settings === null || settings === void 0 ? void 0 : settings.preventScroll) !== null && _f !== void 0 ? _f : false;
|
|
141
145
|
const preventInitialFocus = focusInStrategy === 'initial' && (settings === null || settings === void 0 ? void 0 : settings.activeDescendantControl);
|
|
142
146
|
function getFirstFocusableElement() {
|
|
143
|
-
return focusableElements
|
|
147
|
+
return focusableElements.get(0);
|
|
144
148
|
}
|
|
145
149
|
function isActiveDescendantInputFocused() {
|
|
146
150
|
return document.activeElement === activeDescendantControl;
|
|
@@ -148,6 +152,7 @@ function focusZone(container, settings) {
|
|
|
148
152
|
function updateFocusedElement(to, directlyActivated = false) {
|
|
149
153
|
const from = currentFocusedElement;
|
|
150
154
|
currentFocusedElement = to;
|
|
155
|
+
wasDirectlyActivated = directlyActivated;
|
|
151
156
|
if (activeDescendantControl) {
|
|
152
157
|
if (to && isActiveDescendantInputFocused()) {
|
|
153
158
|
setActiveDescendant(from, to, directlyActivated);
|
|
@@ -185,36 +190,41 @@ function focusZone(container, settings) {
|
|
|
185
190
|
activeDescendantControl === null || activeDescendantControl === void 0 ? void 0 : activeDescendantControl.removeAttribute('aria-activedescendant');
|
|
186
191
|
container.removeAttribute(hasActiveDescendantAttribute);
|
|
187
192
|
previouslyActiveElement === null || previouslyActiveElement === void 0 ? void 0 : previouslyActiveElement.removeAttribute(isActiveDescendantAttribute);
|
|
188
|
-
|
|
189
|
-
|
|
193
|
+
const items = container.querySelectorAll(`[${isActiveDescendantAttribute}]`);
|
|
194
|
+
for (let i = 0; i < items.length; i++) {
|
|
195
|
+
items[i].removeAttribute(isActiveDescendantAttribute);
|
|
190
196
|
}
|
|
191
197
|
activeDescendantCallback === null || activeDescendantCallback === void 0 ? void 0 : activeDescendantCallback(undefined, previouslyActiveElement, false);
|
|
192
198
|
}
|
|
193
199
|
function beginFocusManagement(...elements) {
|
|
194
|
-
const filteredElements =
|
|
200
|
+
const filteredElements = (settings === null || settings === void 0 ? void 0 : settings.focusableElementFilter)
|
|
201
|
+
? elements.filter(e => settings.focusableElementFilter(e))
|
|
202
|
+
: elements;
|
|
195
203
|
if (filteredElements.length === 0) {
|
|
196
204
|
return;
|
|
197
205
|
}
|
|
198
|
-
|
|
206
|
+
const insertionIndex = findInsertionIndex(filteredElements);
|
|
207
|
+
focusableElements.insertAt(insertionIndex, ...filteredElements);
|
|
199
208
|
for (const element of filteredElements) {
|
|
200
209
|
if (!savedTabIndex.has(element)) {
|
|
201
210
|
savedTabIndex.set(element, element.getAttribute('tabindex'));
|
|
202
211
|
}
|
|
203
212
|
element.setAttribute('tabindex', '-1');
|
|
204
213
|
}
|
|
205
|
-
|
|
214
|
+
const shouldFocusPrepended = focusPrependedElements && insertionIndex === 0 && !wasDirectlyActivated;
|
|
215
|
+
if (!preventInitialFocus && (!currentFocusedElement || shouldFocusPrepended)) {
|
|
206
216
|
updateFocusedElement(getFirstFocusableElement());
|
|
207
217
|
}
|
|
208
218
|
}
|
|
209
219
|
function findInsertionIndex(elementsToInsert) {
|
|
210
220
|
const firstElementToInsert = elementsToInsert[0];
|
|
211
|
-
if (focusableElements.
|
|
221
|
+
if (focusableElements.size === 0)
|
|
212
222
|
return 0;
|
|
213
223
|
let iMin = 0;
|
|
214
|
-
let iMax = focusableElements.
|
|
224
|
+
let iMax = focusableElements.size - 1;
|
|
215
225
|
while (iMin <= iMax) {
|
|
216
226
|
const i = Math.floor((iMin + iMax) / 2);
|
|
217
|
-
const element = focusableElements
|
|
227
|
+
const element = focusableElements.get(i);
|
|
218
228
|
if (followsInDocument(firstElementToInsert, element)) {
|
|
219
229
|
iMax = i - 1;
|
|
220
230
|
}
|
|
@@ -229,10 +239,7 @@ function focusZone(container, settings) {
|
|
|
229
239
|
}
|
|
230
240
|
function endFocusManagement(...elements) {
|
|
231
241
|
for (const element of elements) {
|
|
232
|
-
|
|
233
|
-
if (focusableElementIndex >= 0) {
|
|
234
|
-
focusableElements.splice(focusableElementIndex, 1);
|
|
235
|
-
}
|
|
242
|
+
focusableElements.delete(element);
|
|
236
243
|
const savedIndex = savedTabIndex.get(element);
|
|
237
244
|
if (savedIndex !== undefined) {
|
|
238
245
|
if (savedIndex === null) {
|
|
@@ -259,29 +266,61 @@ function focusZone(container, settings) {
|
|
|
259
266
|
if (!preventInitialFocus)
|
|
260
267
|
updateFocusedElement(initialElement);
|
|
261
268
|
const observer = new MutationObserver(mutations => {
|
|
269
|
+
const elementsToRemove = new Set();
|
|
270
|
+
const elementsToAdd = new Set();
|
|
271
|
+
const attributeRemovals = new Set();
|
|
272
|
+
const attributeAdditions = new Set();
|
|
262
273
|
for (const mutation of mutations) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
274
|
+
if (mutation.type === 'childList') {
|
|
275
|
+
for (const removedNode of mutation.removedNodes) {
|
|
276
|
+
if (removedNode instanceof HTMLElement) {
|
|
277
|
+
elementsToRemove.add(removedNode);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (const addedNode of mutation.addedNodes) {
|
|
281
|
+
if (addedNode instanceof HTMLElement) {
|
|
282
|
+
elementsToAdd.add(addedNode);
|
|
283
|
+
}
|
|
266
284
|
}
|
|
267
285
|
}
|
|
268
|
-
if (mutation.type === 'attributes' && mutation.
|
|
269
|
-
|
|
270
|
-
|
|
286
|
+
else if (mutation.type === 'attributes' && mutation.target instanceof HTMLElement) {
|
|
287
|
+
const attributeName = mutation.attributeName;
|
|
288
|
+
const hasAttribute = attributeName ? mutation.target.hasAttribute(attributeName) : false;
|
|
289
|
+
if (hasAttribute) {
|
|
290
|
+
attributeRemovals.add(mutation.target);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
attributeAdditions.add(mutation.target);
|
|
271
294
|
}
|
|
272
295
|
}
|
|
273
296
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
297
|
+
if (elementsToRemove.size > 0) {
|
|
298
|
+
const toRemove = [];
|
|
299
|
+
for (const node of elementsToRemove) {
|
|
300
|
+
for (const el of iterateFocusableElements(node)) {
|
|
301
|
+
toRemove.push(el);
|
|
278
302
|
}
|
|
279
303
|
}
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
304
|
+
if (toRemove.length > 0) {
|
|
305
|
+
endFocusManagement(...toRemove);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (attributeRemovals.size > 0) {
|
|
309
|
+
endFocusManagement(...attributeRemovals);
|
|
310
|
+
}
|
|
311
|
+
if (elementsToAdd.size > 0) {
|
|
312
|
+
const toAdd = [];
|
|
313
|
+
for (const node of elementsToAdd) {
|
|
314
|
+
for (const el of iterateFocusableElements(node, iterateFocusableElementsOptions)) {
|
|
315
|
+
toAdd.push(el);
|
|
283
316
|
}
|
|
284
317
|
}
|
|
318
|
+
if (toAdd.length > 0) {
|
|
319
|
+
beginFocusManagement(...toAdd);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (attributeAdditions.size > 0) {
|
|
323
|
+
beginFocusManagement(...attributeAdditions);
|
|
285
324
|
}
|
|
286
325
|
});
|
|
287
326
|
observer.observe(container, {
|
|
@@ -291,9 +330,11 @@ function focusZone(container, settings) {
|
|
|
291
330
|
attributeOldValue: true,
|
|
292
331
|
});
|
|
293
332
|
const controller = new AbortController();
|
|
294
|
-
const signal = (
|
|
333
|
+
const signal = (_g = settings === null || settings === void 0 ? void 0 : settings.abortSignal) !== null && _g !== void 0 ? _g : controller.signal;
|
|
295
334
|
signal.addEventListener('abort', () => {
|
|
335
|
+
observer.disconnect();
|
|
296
336
|
endFocusManagement(...focusableElements);
|
|
337
|
+
focusableElements.clear();
|
|
297
338
|
});
|
|
298
339
|
let elementIndexFocusedByClick = undefined;
|
|
299
340
|
container.addEventListener('mousedown', event => {
|
|
@@ -303,7 +344,7 @@ function focusZone(container, settings) {
|
|
|
303
344
|
}, { signal });
|
|
304
345
|
if (activeDescendantControl) {
|
|
305
346
|
container.addEventListener('focusin', event => {
|
|
306
|
-
if (event.target instanceof HTMLElement && focusableElements.
|
|
347
|
+
if (event.target instanceof HTMLElement && focusableElements.has(event.target)) {
|
|
307
348
|
activeDescendantControl.focus({ preventScroll });
|
|
308
349
|
updateFocusedElement(event.target);
|
|
309
350
|
}
|
|
@@ -313,6 +354,10 @@ function focusZone(container, settings) {
|
|
|
313
354
|
if (!(target instanceof Node)) {
|
|
314
355
|
return;
|
|
315
356
|
}
|
|
357
|
+
if (target instanceof HTMLElement && focusableElements.has(target)) {
|
|
358
|
+
updateFocusedElement(target);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
316
361
|
const focusableElement = focusableElements.find(element => element.contains(target));
|
|
317
362
|
if (focusableElement) {
|
|
318
363
|
updateFocusedElement(focusableElement);
|
|
@@ -337,8 +382,9 @@ function focusZone(container, settings) {
|
|
|
337
382
|
if (event.target instanceof HTMLElement) {
|
|
338
383
|
if (elementIndexFocusedByClick !== undefined) {
|
|
339
384
|
if (elementIndexFocusedByClick >= 0) {
|
|
340
|
-
|
|
341
|
-
|
|
385
|
+
const clickedElement = focusableElements.get(elementIndexFocusedByClick);
|
|
386
|
+
if (clickedElement && clickedElement !== currentFocusedElement) {
|
|
387
|
+
updateFocusedElement(clickedElement);
|
|
342
388
|
}
|
|
343
389
|
}
|
|
344
390
|
elementIndexFocusedByClick = undefined;
|
|
@@ -349,8 +395,8 @@ function focusZone(container, settings) {
|
|
|
349
395
|
}
|
|
350
396
|
else if (focusInStrategy === 'closest' || focusInStrategy === 'first') {
|
|
351
397
|
if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
|
|
352
|
-
const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.
|
|
353
|
-
const targetElement = focusableElements
|
|
398
|
+
const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.size - 1 : 0;
|
|
399
|
+
const targetElement = focusableElements.get(targetElementIndex);
|
|
354
400
|
targetElement === null || targetElement === void 0 ? void 0 : targetElement.focus({ preventScroll });
|
|
355
401
|
return;
|
|
356
402
|
}
|
|
@@ -361,8 +407,7 @@ function focusZone(container, settings) {
|
|
|
361
407
|
else if (typeof focusInStrategy === 'function') {
|
|
362
408
|
if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
|
|
363
409
|
const elementToFocus = focusInStrategy(event.relatedTarget);
|
|
364
|
-
|
|
365
|
-
if (requestedFocusElementIndex >= 0 && elementToFocus instanceof HTMLElement) {
|
|
410
|
+
if (elementToFocus && focusableElements.has(elementToFocus)) {
|
|
366
411
|
elementToFocus.focus({ preventScroll });
|
|
367
412
|
return;
|
|
368
413
|
}
|
|
@@ -421,26 +466,26 @@ function focusZone(container, settings) {
|
|
|
421
466
|
nextFocusedIndex += 1;
|
|
422
467
|
}
|
|
423
468
|
else {
|
|
424
|
-
nextFocusedIndex = focusableElements.
|
|
469
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
425
470
|
}
|
|
426
471
|
if (nextFocusedIndex < 0) {
|
|
427
472
|
if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
|
|
428
|
-
nextFocusedIndex = focusableElements.
|
|
473
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
429
474
|
}
|
|
430
475
|
else {
|
|
431
476
|
nextFocusedIndex = 0;
|
|
432
477
|
}
|
|
433
478
|
}
|
|
434
|
-
if (nextFocusedIndex >= focusableElements.
|
|
479
|
+
if (nextFocusedIndex >= focusableElements.size) {
|
|
435
480
|
if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
|
|
436
481
|
nextFocusedIndex = 0;
|
|
437
482
|
}
|
|
438
483
|
else {
|
|
439
|
-
nextFocusedIndex = focusableElements.
|
|
484
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
440
485
|
}
|
|
441
486
|
}
|
|
442
487
|
if (lastFocusedIndex !== nextFocusedIndex) {
|
|
443
|
-
nextElementToFocus = focusableElements
|
|
488
|
+
nextElementToFocus = focusableElements.get(nextFocusedIndex);
|
|
444
489
|
}
|
|
445
490
|
}
|
|
446
491
|
if (activeDescendantControl) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class IndexedSet<T> {
|
|
2
|
+
private _items;
|
|
3
|
+
private _itemSet;
|
|
4
|
+
insertAt(index: number, ...elements: T[]): void;
|
|
5
|
+
delete(element: T): boolean;
|
|
6
|
+
has(element: T): boolean;
|
|
7
|
+
indexOf(element: T): number;
|
|
8
|
+
get(index: number): T | undefined;
|
|
9
|
+
get size(): number;
|
|
10
|
+
[Symbol.iterator](): Iterator<T>;
|
|
11
|
+
clear(): void;
|
|
12
|
+
find(predicate: (element: T) => boolean): T | undefined;
|
|
13
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
class IndexedSet {
|
|
2
|
+
constructor() {
|
|
3
|
+
this._items = [];
|
|
4
|
+
this._itemSet = new Set();
|
|
5
|
+
}
|
|
6
|
+
insertAt(index, ...elements) {
|
|
7
|
+
const newElements = elements.filter(e => !this._itemSet.has(e));
|
|
8
|
+
if (newElements.length === 0)
|
|
9
|
+
return;
|
|
10
|
+
this._items.splice(index, 0, ...newElements);
|
|
11
|
+
for (const element of newElements) {
|
|
12
|
+
this._itemSet.add(element);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
delete(element) {
|
|
16
|
+
if (!this._itemSet.has(element))
|
|
17
|
+
return false;
|
|
18
|
+
const index = this._items.indexOf(element);
|
|
19
|
+
if (index >= 0) {
|
|
20
|
+
this._items.splice(index, 1);
|
|
21
|
+
}
|
|
22
|
+
this._itemSet.delete(element);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
has(element) {
|
|
26
|
+
return this._itemSet.has(element);
|
|
27
|
+
}
|
|
28
|
+
indexOf(element) {
|
|
29
|
+
if (!this._itemSet.has(element))
|
|
30
|
+
return -1;
|
|
31
|
+
return this._items.indexOf(element);
|
|
32
|
+
}
|
|
33
|
+
get(index) {
|
|
34
|
+
return this._items[index];
|
|
35
|
+
}
|
|
36
|
+
get size() {
|
|
37
|
+
return this._items.length;
|
|
38
|
+
}
|
|
39
|
+
[Symbol.iterator]() {
|
|
40
|
+
return this._items[Symbol.iterator]();
|
|
41
|
+
}
|
|
42
|
+
clear() {
|
|
43
|
+
this._items = [];
|
|
44
|
+
this._itemSet.clear();
|
|
45
|
+
}
|
|
46
|
+
find(predicate) {
|
|
47
|
+
return this._items.find(predicate);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { IndexedSet };
|
|
@@ -31,24 +31,32 @@ function* iterateFocusableElements(container, options = {}) {
|
|
|
31
31
|
function getFocusableChild(container, lastChild = false) {
|
|
32
32
|
return iterateFocusableElements(container, { reverse: lastChild, strict: true, onlyTabbable: true }).next().value;
|
|
33
33
|
}
|
|
34
|
+
const DISABLEABLE_TAGS = new Set(['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTGROUP', 'OPTION', 'FIELDSET']);
|
|
34
35
|
function isFocusable(elem, strict = false) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
if (elem.hidden)
|
|
37
|
+
return false;
|
|
38
|
+
if (elem.classList.contains('sentinel'))
|
|
39
|
+
return false;
|
|
40
|
+
if (elem instanceof HTMLInputElement && elem.type === 'hidden')
|
|
41
|
+
return false;
|
|
42
|
+
if (DISABLEABLE_TAGS.has(elem.tagName) && elem.disabled)
|
|
41
43
|
return false;
|
|
42
|
-
}
|
|
43
44
|
if (strict) {
|
|
45
|
+
const offsetWidth = elem.offsetWidth;
|
|
46
|
+
const offsetHeight = elem.offsetHeight;
|
|
47
|
+
const offsetParent = elem.offsetParent;
|
|
48
|
+
if (offsetWidth === 0 || offsetHeight === 0)
|
|
49
|
+
return false;
|
|
44
50
|
const style = getComputedStyle(elem);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
if (style.display === 'none')
|
|
52
|
+
return false;
|
|
53
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse')
|
|
54
|
+
return false;
|
|
55
|
+
const position = style.position;
|
|
56
|
+
if (!offsetParent && position !== 'fixed' && position !== 'sticky')
|
|
57
|
+
return false;
|
|
58
|
+
if (elem.getClientRects().length === 0)
|
|
50
59
|
return false;
|
|
51
|
-
}
|
|
52
60
|
}
|
|
53
61
|
if (elem.getAttribute('tabindex') != null) {
|
|
54
62
|
return true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primer/behaviors",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.1-rc.69df32c",
|
|
4
4
|
"description": "Shared behaviors for JavaScript components",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"@testing-library/react": "^16.0.0",
|
|
83
83
|
"@testing-library/user-event": "^14.5.1",
|
|
84
84
|
"@types/jest": "^30.0.0",
|
|
85
|
-
"@types/node": "^
|
|
85
|
+
"@types/node": "^25.0.0",
|
|
86
86
|
"@types/react": "^19.0.1",
|
|
87
87
|
"@types/react-dom": "^19.2.3",
|
|
88
88
|
"clsx": "^2.1.1",
|