@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.
@@ -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 && getComputedStyle(parentNode).position !== 'static') {
32
- return parentNode;
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 (!(parentNode instanceof Element)) {
57
- break;
58
- }
59
- const parentNodeStyle = getComputedStyle(parentNode);
60
- if (parentNodeStyle.overflow !== 'visible') {
61
- break;
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 [borderTop, borderLeft, borderRight, borderBottom] = [
69
- elemStyle.borderTopWidth,
70
- elemStyle.borderLeftWidth,
71
- elemStyle.borderRightWidth,
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,
@@ -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 sentinelChildren = Array.from(mutation.addedNodes).filter(e => e instanceof HTMLElement && e.classList.contains('sentinel') && e.tagName === 'SPAN');
27
- if (sentinelChildren.length) {
28
- return;
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 = document.createElement('span');
50
- sentinelStart.setAttribute('class', 'sentinel');
51
- sentinelStart.setAttribute('tabindex', '0');
52
- sentinelStart.setAttribute('aria-hidden', 'true');
53
- sentinelStart.onfocus = () => {
54
- const lastFocusableChild = iterateFocusableElements.getFocusableChild(container, true);
55
- lastFocusableChild === null || lastFocusableChild === void 0 ? void 0 : lastFocusableChild.focus();
56
- };
57
- const sentinelEnd = document.createElement('span');
58
- sentinelEnd.setAttribute('class', 'sentinel');
59
- sentinelEnd.setAttribute('tabindex', '0');
60
- sentinelEnd.setAttribute('aria-hidden', 'true');
61
- sentinelEnd.onfocus = () => {
62
- const firstFocusableChild = iterateFocusableElements.getFocusableChild(container);
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
  }
@@ -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 keyLength = [...key].length;
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 && (keyLength === 1 || key === 'Home' || key === 'End')) {
89
+ if (isEditable && (isSingleChar || key === 'Home' || key === 'End')) {
89
90
  return true;
90
91
  }
91
92
  if (isSelect) {
92
- if (key === 'ArrowDown' && userAgent.isMacOS() && !keyboardEvent.metaKey) {
93
+ const isMac = userAgent.isMacOS();
94
+ if (key === 'ArrowDown' && isMac && !keyboardEvent.metaKey) {
93
95
  return true;
94
96
  }
95
- if (key === 'ArrowDown' && !userAgent.isMacOS() && keyboardEvent.altKey) {
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[0];
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
- for (const item of container.querySelectorAll(`[${isActiveDescendantAttribute}]`)) {
191
- item === null || item === void 0 ? void 0 : item.removeAttribute(isActiveDescendantAttribute);
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 = elements.filter(e => { var _a, _b; return (_b = (_a = settings === null || settings === void 0 ? void 0 : settings.focusableElementFilter) === null || _a === void 0 ? void 0 : _a.call(settings, e)) !== null && _b !== void 0 ? _b : true; });
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.splice(findInsertionIndex(filteredElements), 0, ...filteredElements);
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.length === 0)
218
+ if (focusableElements.size === 0)
214
219
  return 0;
215
220
  let iMin = 0;
216
- let iMax = focusableElements.length - 1;
221
+ let iMax = focusableElements.size - 1;
217
222
  while (iMin <= iMax) {
218
223
  const i = Math.floor((iMin + iMax) / 2);
219
- const element = focusableElements[i];
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
- const focusableElementIndex = focusableElements.indexOf(element);
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
- for (const removedNode of mutation.removedNodes) {
266
- if (removedNode instanceof HTMLElement) {
267
- endFocusManagement(...iterateFocusableElements.iterateFocusableElements(removedNode));
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.oldValue === null) {
271
- if (mutation.target instanceof HTMLElement) {
272
- endFocusManagement(mutation.target);
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
- for (const mutation of mutations) {
277
- for (const addedNode of mutation.addedNodes) {
278
- if (addedNode instanceof HTMLElement) {
279
- beginFocusManagement(...iterateFocusableElements.iterateFocusableElements(addedNode, iterateFocusableElementsOptions));
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 (mutation.type === 'attributes' && mutation.oldValue !== null) {
283
- if (mutation.target instanceof HTMLElement) {
284
- beginFocusManagement(mutation.target);
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.includes(event.target)) {
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
- if (focusableElements[elementIndexFocusedByClick] !== currentFocusedElement) {
343
- updateFocusedElement(focusableElements[elementIndexFocusedByClick]);
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.length - 1 : 0;
355
- const targetElement = focusableElements[targetElementIndex];
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
- const requestedFocusElementIndex = elementToFocus ? focusableElements.indexOf(elementToFocus) : -1;
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.length - 1;
466
+ nextFocusedIndex = focusableElements.size - 1;
427
467
  }
428
468
  if (nextFocusedIndex < 0) {
429
469
  if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
430
- nextFocusedIndex = focusableElements.length - 1;
470
+ nextFocusedIndex = focusableElements.size - 1;
431
471
  }
432
472
  else {
433
473
  nextFocusedIndex = 0;
434
474
  }
435
475
  }
436
- if (nextFocusedIndex >= focusableElements.length) {
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.length - 1;
481
+ nextFocusedIndex = focusableElements.size - 1;
442
482
  }
443
483
  }
444
484
  if (lastFocusedIndex !== nextFocusedIndex) {
445
- nextElementToFocus = focusableElements[nextFocusedIndex];
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
- const disabledAttrInert = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTGROUP', 'OPTION', 'FIELDSET'].includes(elem.tagName) &&
38
- elem.disabled;
39
- const hiddenInert = elem.hidden;
40
- const hiddenInputInert = elem instanceof HTMLInputElement && elem.type === 'hidden';
41
- const sentinelInert = elem.classList.contains('sentinel');
42
- if (disabledAttrInert || hiddenInert || hiddenInputInert || sentinelInert) {
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
- const sizeInert = elem.offsetWidth === 0 || elem.offsetHeight === 0;
48
- const visibilityInert = ['hidden', 'collapse'].includes(style.visibility);
49
- const displayInert = style.display === 'none' || !elem.offsetParent;
50
- const clientRectsInert = elem.getClientRects().length === 0;
51
- if (sizeInert || visibilityInert || clientRectsInert || displayInert) {
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 && getComputedStyle(parentNode).position !== 'static') {
30
- return parentNode;
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 (!(parentNode instanceof Element)) {
55
- break;
56
- }
57
- const parentNodeStyle = getComputedStyle(parentNode);
58
- if (parentNodeStyle.overflow !== 'visible') {
59
- break;
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 [borderTop, borderLeft, borderRight, borderBottom] = [
67
- elemStyle.borderTopWidth,
68
- elemStyle.borderLeftWidth,
69
- elemStyle.borderRightWidth,
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,
@@ -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 sentinelChildren = Array.from(mutation.addedNodes).filter(e => e instanceof HTMLElement && e.classList.contains('sentinel') && e.tagName === 'SPAN');
25
- if (sentinelChildren.length) {
26
- return;
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 = document.createElement('span');
48
- sentinelStart.setAttribute('class', 'sentinel');
49
- sentinelStart.setAttribute('tabindex', '0');
50
- sentinelStart.setAttribute('aria-hidden', 'true');
51
- sentinelStart.onfocus = () => {
52
- const lastFocusableChild = getFocusableChild(container, true);
53
- lastFocusableChild === null || lastFocusableChild === void 0 ? void 0 : lastFocusableChild.focus();
54
- };
55
- const sentinelEnd = document.createElement('span');
56
- sentinelEnd.setAttribute('class', 'sentinel');
57
- sentinelEnd.setAttribute('tabindex', '0');
58
- sentinelEnd.setAttribute('aria-hidden', 'true');
59
- sentinelEnd.onfocus = () => {
60
- const firstFocusableChild = getFocusableChild(container);
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
  }
@@ -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 keyLength = [...key].length;
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 && (keyLength === 1 || key === 'Home' || key === 'End')) {
87
+ if (isEditable && (isSingleChar || key === 'Home' || key === 'End')) {
87
88
  return true;
88
89
  }
89
90
  if (isSelect) {
90
- if (key === 'ArrowDown' && isMacOS() && !keyboardEvent.metaKey) {
91
+ const isMac = isMacOS();
92
+ if (key === 'ArrowDown' && isMac && !keyboardEvent.metaKey) {
91
93
  return true;
92
94
  }
93
- if (key === 'ArrowDown' && !isMacOS() && keyboardEvent.altKey) {
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[0];
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
- for (const item of container.querySelectorAll(`[${isActiveDescendantAttribute}]`)) {
189
- item === null || item === void 0 ? void 0 : item.removeAttribute(isActiveDescendantAttribute);
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 = elements.filter(e => { var _a, _b; return (_b = (_a = settings === null || settings === void 0 ? void 0 : settings.focusableElementFilter) === null || _a === void 0 ? void 0 : _a.call(settings, e)) !== null && _b !== void 0 ? _b : true; });
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.splice(findInsertionIndex(filteredElements), 0, ...filteredElements);
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.length === 0)
216
+ if (focusableElements.size === 0)
212
217
  return 0;
213
218
  let iMin = 0;
214
- let iMax = focusableElements.length - 1;
219
+ let iMax = focusableElements.size - 1;
215
220
  while (iMin <= iMax) {
216
221
  const i = Math.floor((iMin + iMax) / 2);
217
- const element = focusableElements[i];
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
- const focusableElementIndex = focusableElements.indexOf(element);
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
- for (const removedNode of mutation.removedNodes) {
264
- if (removedNode instanceof HTMLElement) {
265
- endFocusManagement(...iterateFocusableElements(removedNode));
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.oldValue === null) {
269
- if (mutation.target instanceof HTMLElement) {
270
- endFocusManagement(mutation.target);
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
- for (const mutation of mutations) {
275
- for (const addedNode of mutation.addedNodes) {
276
- if (addedNode instanceof HTMLElement) {
277
- beginFocusManagement(...iterateFocusableElements(addedNode, iterateFocusableElementsOptions));
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 (mutation.type === 'attributes' && mutation.oldValue !== null) {
281
- if (mutation.target instanceof HTMLElement) {
282
- beginFocusManagement(mutation.target);
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.includes(event.target)) {
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
- if (focusableElements[elementIndexFocusedByClick] !== currentFocusedElement) {
341
- updateFocusedElement(focusableElements[elementIndexFocusedByClick]);
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.length - 1 : 0;
353
- const targetElement = focusableElements[targetElementIndex];
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
- const requestedFocusElementIndex = elementToFocus ? focusableElements.indexOf(elementToFocus) : -1;
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.length - 1;
464
+ nextFocusedIndex = focusableElements.size - 1;
425
465
  }
426
466
  if (nextFocusedIndex < 0) {
427
467
  if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
428
- nextFocusedIndex = focusableElements.length - 1;
468
+ nextFocusedIndex = focusableElements.size - 1;
429
469
  }
430
470
  else {
431
471
  nextFocusedIndex = 0;
432
472
  }
433
473
  }
434
- if (nextFocusedIndex >= focusableElements.length) {
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.length - 1;
479
+ nextFocusedIndex = focusableElements.size - 1;
440
480
  }
441
481
  }
442
482
  if (lastFocusedIndex !== nextFocusedIndex) {
443
- nextElementToFocus = focusableElements[nextFocusedIndex];
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
- const disabledAttrInert = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTGROUP', 'OPTION', 'FIELDSET'].includes(elem.tagName) &&
36
- elem.disabled;
37
- const hiddenInert = elem.hidden;
38
- const hiddenInputInert = elem instanceof HTMLInputElement && elem.type === 'hidden';
39
- const sentinelInert = elem.classList.contains('sentinel');
40
- if (disabledAttrInert || hiddenInert || hiddenInputInert || sentinelInert) {
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
- const sizeInert = elem.offsetWidth === 0 || elem.offsetHeight === 0;
46
- const visibilityInert = ['hidden', 'collapse'].includes(style.visibility);
47
- const displayInert = style.display === 'none' || !elem.offsetParent;
48
- const clientRectsInert = elem.getClientRects().length === 0;
49
- if (sizeInert || visibilityInert || clientRectsInert || displayInert) {
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.0-rc.8649bfb",
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": "^24.0.10",
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",