@primer/behaviors 1.9.0-rc.8649bfb → 1.9.1-rc.5011fc8
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.js +82 -42
- 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.mjs +82 -42
- 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.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;
|
|
@@ -130,7 +132,7 @@ const activeDescendantActivatedIndirectly = 'activated-indirectly';
|
|
|
130
132
|
const hasActiveDescendantAttribute = 'data-has-active-descendant';
|
|
131
133
|
function focusZone(container, settings) {
|
|
132
134
|
var _a, _b, _c, _d, _e, _f;
|
|
133
|
-
const focusableElements =
|
|
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';
|
|
@@ -142,7 +144,7 @@ function focusZone(container, settings) {
|
|
|
142
144
|
const preventScroll = (_e = settings === null || settings === void 0 ? void 0 : settings.preventScroll) !== null && _e !== void 0 ? _e : false;
|
|
143
145
|
const preventInitialFocus = focusInStrategy === 'initial' && (settings === null || settings === void 0 ? void 0 : settings.activeDescendantControl);
|
|
144
146
|
function getFirstFocusableElement() {
|
|
145
|
-
return focusableElements
|
|
147
|
+
return focusableElements.get(0);
|
|
146
148
|
}
|
|
147
149
|
function isActiveDescendantInputFocused() {
|
|
148
150
|
return document.activeElement === activeDescendantControl;
|
|
@@ -187,17 +189,20 @@ function focusZone(container, settings) {
|
|
|
187
189
|
activeDescendantControl === null || activeDescendantControl === void 0 ? void 0 : activeDescendantControl.removeAttribute('aria-activedescendant');
|
|
188
190
|
container.removeAttribute(hasActiveDescendantAttribute);
|
|
189
191
|
previouslyActiveElement === null || previouslyActiveElement === void 0 ? void 0 : previouslyActiveElement.removeAttribute(isActiveDescendantAttribute);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
+
const items = container.querySelectorAll(`[${isActiveDescendantAttribute}]`);
|
|
193
|
+
for (let i = 0; i < items.length; i++) {
|
|
194
|
+
items[i].removeAttribute(isActiveDescendantAttribute);
|
|
192
195
|
}
|
|
193
196
|
activeDescendantCallback === null || activeDescendantCallback === void 0 ? void 0 : activeDescendantCallback(undefined, previouslyActiveElement, false);
|
|
194
197
|
}
|
|
195
198
|
function beginFocusManagement(...elements) {
|
|
196
|
-
const filteredElements =
|
|
199
|
+
const filteredElements = (settings === null || settings === void 0 ? void 0 : settings.focusableElementFilter)
|
|
200
|
+
? elements.filter(e => settings.focusableElementFilter(e))
|
|
201
|
+
: elements;
|
|
197
202
|
if (filteredElements.length === 0) {
|
|
198
203
|
return;
|
|
199
204
|
}
|
|
200
|
-
focusableElements.
|
|
205
|
+
focusableElements.insertAt(findInsertionIndex(filteredElements), ...filteredElements);
|
|
201
206
|
for (const element of filteredElements) {
|
|
202
207
|
if (!savedTabIndex.has(element)) {
|
|
203
208
|
savedTabIndex.set(element, element.getAttribute('tabindex'));
|
|
@@ -210,13 +215,13 @@ function focusZone(container, settings) {
|
|
|
210
215
|
}
|
|
211
216
|
function findInsertionIndex(elementsToInsert) {
|
|
212
217
|
const firstElementToInsert = elementsToInsert[0];
|
|
213
|
-
if (focusableElements.
|
|
218
|
+
if (focusableElements.size === 0)
|
|
214
219
|
return 0;
|
|
215
220
|
let iMin = 0;
|
|
216
|
-
let iMax = focusableElements.
|
|
221
|
+
let iMax = focusableElements.size - 1;
|
|
217
222
|
while (iMin <= iMax) {
|
|
218
223
|
const i = Math.floor((iMin + iMax) / 2);
|
|
219
|
-
const element = focusableElements
|
|
224
|
+
const element = focusableElements.get(i);
|
|
220
225
|
if (followsInDocument(firstElementToInsert, element)) {
|
|
221
226
|
iMax = i - 1;
|
|
222
227
|
}
|
|
@@ -231,10 +236,7 @@ function focusZone(container, settings) {
|
|
|
231
236
|
}
|
|
232
237
|
function endFocusManagement(...elements) {
|
|
233
238
|
for (const element of elements) {
|
|
234
|
-
|
|
235
|
-
if (focusableElementIndex >= 0) {
|
|
236
|
-
focusableElements.splice(focusableElementIndex, 1);
|
|
237
|
-
}
|
|
239
|
+
focusableElements.delete(element);
|
|
238
240
|
const savedIndex = savedTabIndex.get(element);
|
|
239
241
|
if (savedIndex !== undefined) {
|
|
240
242
|
if (savedIndex === null) {
|
|
@@ -261,29 +263,61 @@ function focusZone(container, settings) {
|
|
|
261
263
|
if (!preventInitialFocus)
|
|
262
264
|
updateFocusedElement(initialElement);
|
|
263
265
|
const observer = new MutationObserver(mutations => {
|
|
266
|
+
const elementsToRemove = new Set();
|
|
267
|
+
const elementsToAdd = new Set();
|
|
268
|
+
const attributeRemovals = new Set();
|
|
269
|
+
const attributeAdditions = new Set();
|
|
264
270
|
for (const mutation of mutations) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
271
|
+
if (mutation.type === 'childList') {
|
|
272
|
+
for (const removedNode of mutation.removedNodes) {
|
|
273
|
+
if (removedNode instanceof HTMLElement) {
|
|
274
|
+
elementsToRemove.add(removedNode);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const addedNode of mutation.addedNodes) {
|
|
278
|
+
if (addedNode instanceof HTMLElement) {
|
|
279
|
+
elementsToAdd.add(addedNode);
|
|
280
|
+
}
|
|
268
281
|
}
|
|
269
282
|
}
|
|
270
|
-
if (mutation.type === 'attributes' && mutation.
|
|
271
|
-
|
|
272
|
-
|
|
283
|
+
else if (mutation.type === 'attributes' && mutation.target instanceof HTMLElement) {
|
|
284
|
+
const attributeName = mutation.attributeName;
|
|
285
|
+
const hasAttribute = attributeName ? mutation.target.hasAttribute(attributeName) : false;
|
|
286
|
+
if (hasAttribute) {
|
|
287
|
+
attributeRemovals.add(mutation.target);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
attributeAdditions.add(mutation.target);
|
|
273
291
|
}
|
|
274
292
|
}
|
|
275
293
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
294
|
+
if (elementsToRemove.size > 0) {
|
|
295
|
+
const toRemove = [];
|
|
296
|
+
for (const node of elementsToRemove) {
|
|
297
|
+
for (const el of iterateFocusableElements.iterateFocusableElements(node)) {
|
|
298
|
+
toRemove.push(el);
|
|
280
299
|
}
|
|
281
300
|
}
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
301
|
+
if (toRemove.length > 0) {
|
|
302
|
+
endFocusManagement(...toRemove);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (attributeRemovals.size > 0) {
|
|
306
|
+
endFocusManagement(...attributeRemovals);
|
|
307
|
+
}
|
|
308
|
+
if (elementsToAdd.size > 0) {
|
|
309
|
+
const toAdd = [];
|
|
310
|
+
for (const node of elementsToAdd) {
|
|
311
|
+
for (const el of iterateFocusableElements.iterateFocusableElements(node, iterateFocusableElementsOptions)) {
|
|
312
|
+
toAdd.push(el);
|
|
285
313
|
}
|
|
286
314
|
}
|
|
315
|
+
if (toAdd.length > 0) {
|
|
316
|
+
beginFocusManagement(...toAdd);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (attributeAdditions.size > 0) {
|
|
320
|
+
beginFocusManagement(...attributeAdditions);
|
|
287
321
|
}
|
|
288
322
|
});
|
|
289
323
|
observer.observe(container, {
|
|
@@ -295,7 +329,9 @@ function focusZone(container, settings) {
|
|
|
295
329
|
const controller = new AbortController();
|
|
296
330
|
const signal = (_f = settings === null || settings === void 0 ? void 0 : settings.abortSignal) !== null && _f !== void 0 ? _f : controller.signal;
|
|
297
331
|
signal.addEventListener('abort', () => {
|
|
332
|
+
observer.disconnect();
|
|
298
333
|
endFocusManagement(...focusableElements);
|
|
334
|
+
focusableElements.clear();
|
|
299
335
|
});
|
|
300
336
|
let elementIndexFocusedByClick = undefined;
|
|
301
337
|
container.addEventListener('mousedown', event => {
|
|
@@ -305,7 +341,7 @@ function focusZone(container, settings) {
|
|
|
305
341
|
}, { signal });
|
|
306
342
|
if (activeDescendantControl) {
|
|
307
343
|
container.addEventListener('focusin', event => {
|
|
308
|
-
if (event.target instanceof HTMLElement && focusableElements.
|
|
344
|
+
if (event.target instanceof HTMLElement && focusableElements.has(event.target)) {
|
|
309
345
|
activeDescendantControl.focus({ preventScroll });
|
|
310
346
|
updateFocusedElement(event.target);
|
|
311
347
|
}
|
|
@@ -315,6 +351,10 @@ function focusZone(container, settings) {
|
|
|
315
351
|
if (!(target instanceof Node)) {
|
|
316
352
|
return;
|
|
317
353
|
}
|
|
354
|
+
if (target instanceof HTMLElement && focusableElements.has(target)) {
|
|
355
|
+
updateFocusedElement(target);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
318
358
|
const focusableElement = focusableElements.find(element => element.contains(target));
|
|
319
359
|
if (focusableElement) {
|
|
320
360
|
updateFocusedElement(focusableElement);
|
|
@@ -339,8 +379,9 @@ function focusZone(container, settings) {
|
|
|
339
379
|
if (event.target instanceof HTMLElement) {
|
|
340
380
|
if (elementIndexFocusedByClick !== undefined) {
|
|
341
381
|
if (elementIndexFocusedByClick >= 0) {
|
|
342
|
-
|
|
343
|
-
|
|
382
|
+
const clickedElement = focusableElements.get(elementIndexFocusedByClick);
|
|
383
|
+
if (clickedElement && clickedElement !== currentFocusedElement) {
|
|
384
|
+
updateFocusedElement(clickedElement);
|
|
344
385
|
}
|
|
345
386
|
}
|
|
346
387
|
elementIndexFocusedByClick = undefined;
|
|
@@ -351,8 +392,8 @@ function focusZone(container, settings) {
|
|
|
351
392
|
}
|
|
352
393
|
else if (focusInStrategy === 'closest' || focusInStrategy === 'first') {
|
|
353
394
|
if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
|
|
354
|
-
const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.
|
|
355
|
-
const targetElement = focusableElements
|
|
395
|
+
const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.size - 1 : 0;
|
|
396
|
+
const targetElement = focusableElements.get(targetElementIndex);
|
|
356
397
|
targetElement === null || targetElement === void 0 ? void 0 : targetElement.focus({ preventScroll });
|
|
357
398
|
return;
|
|
358
399
|
}
|
|
@@ -363,8 +404,7 @@ function focusZone(container, settings) {
|
|
|
363
404
|
else if (typeof focusInStrategy === 'function') {
|
|
364
405
|
if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
|
|
365
406
|
const elementToFocus = focusInStrategy(event.relatedTarget);
|
|
366
|
-
|
|
367
|
-
if (requestedFocusElementIndex >= 0 && elementToFocus instanceof HTMLElement) {
|
|
407
|
+
if (elementToFocus && focusableElements.has(elementToFocus)) {
|
|
368
408
|
elementToFocus.focus({ preventScroll });
|
|
369
409
|
return;
|
|
370
410
|
}
|
|
@@ -423,26 +463,26 @@ function focusZone(container, settings) {
|
|
|
423
463
|
nextFocusedIndex += 1;
|
|
424
464
|
}
|
|
425
465
|
else {
|
|
426
|
-
nextFocusedIndex = focusableElements.
|
|
466
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
427
467
|
}
|
|
428
468
|
if (nextFocusedIndex < 0) {
|
|
429
469
|
if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
|
|
430
|
-
nextFocusedIndex = focusableElements.
|
|
470
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
431
471
|
}
|
|
432
472
|
else {
|
|
433
473
|
nextFocusedIndex = 0;
|
|
434
474
|
}
|
|
435
475
|
}
|
|
436
|
-
if (nextFocusedIndex >= focusableElements.
|
|
476
|
+
if (nextFocusedIndex >= focusableElements.size) {
|
|
437
477
|
if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
|
|
438
478
|
nextFocusedIndex = 0;
|
|
439
479
|
}
|
|
440
480
|
else {
|
|
441
|
-
nextFocusedIndex = focusableElements.
|
|
481
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
442
482
|
}
|
|
443
483
|
}
|
|
444
484
|
if (lastFocusedIndex !== nextFocusedIndex) {
|
|
445
|
-
nextElementToFocus = focusableElements
|
|
485
|
+
nextElementToFocus = focusableElements.get(nextFocusedIndex);
|
|
446
486
|
}
|
|
447
487
|
}
|
|
448
488
|
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.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;
|
|
@@ -128,7 +130,7 @@ const activeDescendantActivatedIndirectly = 'activated-indirectly';
|
|
|
128
130
|
const hasActiveDescendantAttribute = 'data-has-active-descendant';
|
|
129
131
|
function focusZone(container, settings) {
|
|
130
132
|
var _a, _b, _c, _d, _e, _f;
|
|
131
|
-
const focusableElements =
|
|
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';
|
|
@@ -140,7 +142,7 @@ function focusZone(container, settings) {
|
|
|
140
142
|
const preventScroll = (_e = settings === null || settings === void 0 ? void 0 : settings.preventScroll) !== null && _e !== void 0 ? _e : false;
|
|
141
143
|
const preventInitialFocus = focusInStrategy === 'initial' && (settings === null || settings === void 0 ? void 0 : settings.activeDescendantControl);
|
|
142
144
|
function getFirstFocusableElement() {
|
|
143
|
-
return focusableElements
|
|
145
|
+
return focusableElements.get(0);
|
|
144
146
|
}
|
|
145
147
|
function isActiveDescendantInputFocused() {
|
|
146
148
|
return document.activeElement === activeDescendantControl;
|
|
@@ -185,17 +187,20 @@ function focusZone(container, settings) {
|
|
|
185
187
|
activeDescendantControl === null || activeDescendantControl === void 0 ? void 0 : activeDescendantControl.removeAttribute('aria-activedescendant');
|
|
186
188
|
container.removeAttribute(hasActiveDescendantAttribute);
|
|
187
189
|
previouslyActiveElement === null || previouslyActiveElement === void 0 ? void 0 : previouslyActiveElement.removeAttribute(isActiveDescendantAttribute);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
+
const items = container.querySelectorAll(`[${isActiveDescendantAttribute}]`);
|
|
191
|
+
for (let i = 0; i < items.length; i++) {
|
|
192
|
+
items[i].removeAttribute(isActiveDescendantAttribute);
|
|
190
193
|
}
|
|
191
194
|
activeDescendantCallback === null || activeDescendantCallback === void 0 ? void 0 : activeDescendantCallback(undefined, previouslyActiveElement, false);
|
|
192
195
|
}
|
|
193
196
|
function beginFocusManagement(...elements) {
|
|
194
|
-
const filteredElements =
|
|
197
|
+
const filteredElements = (settings === null || settings === void 0 ? void 0 : settings.focusableElementFilter)
|
|
198
|
+
? elements.filter(e => settings.focusableElementFilter(e))
|
|
199
|
+
: elements;
|
|
195
200
|
if (filteredElements.length === 0) {
|
|
196
201
|
return;
|
|
197
202
|
}
|
|
198
|
-
focusableElements.
|
|
203
|
+
focusableElements.insertAt(findInsertionIndex(filteredElements), ...filteredElements);
|
|
199
204
|
for (const element of filteredElements) {
|
|
200
205
|
if (!savedTabIndex.has(element)) {
|
|
201
206
|
savedTabIndex.set(element, element.getAttribute('tabindex'));
|
|
@@ -208,13 +213,13 @@ function focusZone(container, settings) {
|
|
|
208
213
|
}
|
|
209
214
|
function findInsertionIndex(elementsToInsert) {
|
|
210
215
|
const firstElementToInsert = elementsToInsert[0];
|
|
211
|
-
if (focusableElements.
|
|
216
|
+
if (focusableElements.size === 0)
|
|
212
217
|
return 0;
|
|
213
218
|
let iMin = 0;
|
|
214
|
-
let iMax = focusableElements.
|
|
219
|
+
let iMax = focusableElements.size - 1;
|
|
215
220
|
while (iMin <= iMax) {
|
|
216
221
|
const i = Math.floor((iMin + iMax) / 2);
|
|
217
|
-
const element = focusableElements
|
|
222
|
+
const element = focusableElements.get(i);
|
|
218
223
|
if (followsInDocument(firstElementToInsert, element)) {
|
|
219
224
|
iMax = i - 1;
|
|
220
225
|
}
|
|
@@ -229,10 +234,7 @@ function focusZone(container, settings) {
|
|
|
229
234
|
}
|
|
230
235
|
function endFocusManagement(...elements) {
|
|
231
236
|
for (const element of elements) {
|
|
232
|
-
|
|
233
|
-
if (focusableElementIndex >= 0) {
|
|
234
|
-
focusableElements.splice(focusableElementIndex, 1);
|
|
235
|
-
}
|
|
237
|
+
focusableElements.delete(element);
|
|
236
238
|
const savedIndex = savedTabIndex.get(element);
|
|
237
239
|
if (savedIndex !== undefined) {
|
|
238
240
|
if (savedIndex === null) {
|
|
@@ -259,29 +261,61 @@ function focusZone(container, settings) {
|
|
|
259
261
|
if (!preventInitialFocus)
|
|
260
262
|
updateFocusedElement(initialElement);
|
|
261
263
|
const observer = new MutationObserver(mutations => {
|
|
264
|
+
const elementsToRemove = new Set();
|
|
265
|
+
const elementsToAdd = new Set();
|
|
266
|
+
const attributeRemovals = new Set();
|
|
267
|
+
const attributeAdditions = new Set();
|
|
262
268
|
for (const mutation of mutations) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
269
|
+
if (mutation.type === 'childList') {
|
|
270
|
+
for (const removedNode of mutation.removedNodes) {
|
|
271
|
+
if (removedNode instanceof HTMLElement) {
|
|
272
|
+
elementsToRemove.add(removedNode);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const addedNode of mutation.addedNodes) {
|
|
276
|
+
if (addedNode instanceof HTMLElement) {
|
|
277
|
+
elementsToAdd.add(addedNode);
|
|
278
|
+
}
|
|
266
279
|
}
|
|
267
280
|
}
|
|
268
|
-
if (mutation.type === 'attributes' && mutation.
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
else if (mutation.type === 'attributes' && mutation.target instanceof HTMLElement) {
|
|
282
|
+
const attributeName = mutation.attributeName;
|
|
283
|
+
const hasAttribute = attributeName ? mutation.target.hasAttribute(attributeName) : false;
|
|
284
|
+
if (hasAttribute) {
|
|
285
|
+
attributeRemovals.add(mutation.target);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
attributeAdditions.add(mutation.target);
|
|
271
289
|
}
|
|
272
290
|
}
|
|
273
291
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
292
|
+
if (elementsToRemove.size > 0) {
|
|
293
|
+
const toRemove = [];
|
|
294
|
+
for (const node of elementsToRemove) {
|
|
295
|
+
for (const el of iterateFocusableElements(node)) {
|
|
296
|
+
toRemove.push(el);
|
|
278
297
|
}
|
|
279
298
|
}
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
299
|
+
if (toRemove.length > 0) {
|
|
300
|
+
endFocusManagement(...toRemove);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (attributeRemovals.size > 0) {
|
|
304
|
+
endFocusManagement(...attributeRemovals);
|
|
305
|
+
}
|
|
306
|
+
if (elementsToAdd.size > 0) {
|
|
307
|
+
const toAdd = [];
|
|
308
|
+
for (const node of elementsToAdd) {
|
|
309
|
+
for (const el of iterateFocusableElements(node, iterateFocusableElementsOptions)) {
|
|
310
|
+
toAdd.push(el);
|
|
283
311
|
}
|
|
284
312
|
}
|
|
313
|
+
if (toAdd.length > 0) {
|
|
314
|
+
beginFocusManagement(...toAdd);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (attributeAdditions.size > 0) {
|
|
318
|
+
beginFocusManagement(...attributeAdditions);
|
|
285
319
|
}
|
|
286
320
|
});
|
|
287
321
|
observer.observe(container, {
|
|
@@ -293,7 +327,9 @@ function focusZone(container, settings) {
|
|
|
293
327
|
const controller = new AbortController();
|
|
294
328
|
const signal = (_f = settings === null || settings === void 0 ? void 0 : settings.abortSignal) !== null && _f !== void 0 ? _f : controller.signal;
|
|
295
329
|
signal.addEventListener('abort', () => {
|
|
330
|
+
observer.disconnect();
|
|
296
331
|
endFocusManagement(...focusableElements);
|
|
332
|
+
focusableElements.clear();
|
|
297
333
|
});
|
|
298
334
|
let elementIndexFocusedByClick = undefined;
|
|
299
335
|
container.addEventListener('mousedown', event => {
|
|
@@ -303,7 +339,7 @@ function focusZone(container, settings) {
|
|
|
303
339
|
}, { signal });
|
|
304
340
|
if (activeDescendantControl) {
|
|
305
341
|
container.addEventListener('focusin', event => {
|
|
306
|
-
if (event.target instanceof HTMLElement && focusableElements.
|
|
342
|
+
if (event.target instanceof HTMLElement && focusableElements.has(event.target)) {
|
|
307
343
|
activeDescendantControl.focus({ preventScroll });
|
|
308
344
|
updateFocusedElement(event.target);
|
|
309
345
|
}
|
|
@@ -313,6 +349,10 @@ function focusZone(container, settings) {
|
|
|
313
349
|
if (!(target instanceof Node)) {
|
|
314
350
|
return;
|
|
315
351
|
}
|
|
352
|
+
if (target instanceof HTMLElement && focusableElements.has(target)) {
|
|
353
|
+
updateFocusedElement(target);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
316
356
|
const focusableElement = focusableElements.find(element => element.contains(target));
|
|
317
357
|
if (focusableElement) {
|
|
318
358
|
updateFocusedElement(focusableElement);
|
|
@@ -337,8 +377,9 @@ function focusZone(container, settings) {
|
|
|
337
377
|
if (event.target instanceof HTMLElement) {
|
|
338
378
|
if (elementIndexFocusedByClick !== undefined) {
|
|
339
379
|
if (elementIndexFocusedByClick >= 0) {
|
|
340
|
-
|
|
341
|
-
|
|
380
|
+
const clickedElement = focusableElements.get(elementIndexFocusedByClick);
|
|
381
|
+
if (clickedElement && clickedElement !== currentFocusedElement) {
|
|
382
|
+
updateFocusedElement(clickedElement);
|
|
342
383
|
}
|
|
343
384
|
}
|
|
344
385
|
elementIndexFocusedByClick = undefined;
|
|
@@ -349,8 +390,8 @@ function focusZone(container, settings) {
|
|
|
349
390
|
}
|
|
350
391
|
else if (focusInStrategy === 'closest' || focusInStrategy === 'first') {
|
|
351
392
|
if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
|
|
352
|
-
const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.
|
|
353
|
-
const targetElement = focusableElements
|
|
393
|
+
const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.size - 1 : 0;
|
|
394
|
+
const targetElement = focusableElements.get(targetElementIndex);
|
|
354
395
|
targetElement === null || targetElement === void 0 ? void 0 : targetElement.focus({ preventScroll });
|
|
355
396
|
return;
|
|
356
397
|
}
|
|
@@ -361,8 +402,7 @@ function focusZone(container, settings) {
|
|
|
361
402
|
else if (typeof focusInStrategy === 'function') {
|
|
362
403
|
if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
|
|
363
404
|
const elementToFocus = focusInStrategy(event.relatedTarget);
|
|
364
|
-
|
|
365
|
-
if (requestedFocusElementIndex >= 0 && elementToFocus instanceof HTMLElement) {
|
|
405
|
+
if (elementToFocus && focusableElements.has(elementToFocus)) {
|
|
366
406
|
elementToFocus.focus({ preventScroll });
|
|
367
407
|
return;
|
|
368
408
|
}
|
|
@@ -421,26 +461,26 @@ function focusZone(container, settings) {
|
|
|
421
461
|
nextFocusedIndex += 1;
|
|
422
462
|
}
|
|
423
463
|
else {
|
|
424
|
-
nextFocusedIndex = focusableElements.
|
|
464
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
425
465
|
}
|
|
426
466
|
if (nextFocusedIndex < 0) {
|
|
427
467
|
if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
|
|
428
|
-
nextFocusedIndex = focusableElements.
|
|
468
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
429
469
|
}
|
|
430
470
|
else {
|
|
431
471
|
nextFocusedIndex = 0;
|
|
432
472
|
}
|
|
433
473
|
}
|
|
434
|
-
if (nextFocusedIndex >= focusableElements.
|
|
474
|
+
if (nextFocusedIndex >= focusableElements.size) {
|
|
435
475
|
if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
|
|
436
476
|
nextFocusedIndex = 0;
|
|
437
477
|
}
|
|
438
478
|
else {
|
|
439
|
-
nextFocusedIndex = focusableElements.
|
|
479
|
+
nextFocusedIndex = focusableElements.size - 1;
|
|
440
480
|
}
|
|
441
481
|
}
|
|
442
482
|
if (lastFocusedIndex !== nextFocusedIndex) {
|
|
443
|
-
nextElementToFocus = focusableElements
|
|
483
|
+
nextElementToFocus = focusableElements.get(nextFocusedIndex);
|
|
444
484
|
}
|
|
445
485
|
}
|
|
446
486
|
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.5011fc8",
|
|
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",
|