@primer/behaviors 1.9.0 → 1.9.1-rc.69df32c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -28,6 +28,7 @@ export type FocusZoneSettings = IterateFocusableElements & {
28
28
  focusInStrategy?: 'first' | 'closest' | 'previous' | 'initial' | ((previousFocusedElement: Element) => HTMLElement | undefined);
29
29
  preventScroll?: boolean;
30
30
  ignoreHoverEvents?: boolean;
31
+ focusPrependedElements?: boolean;
31
32
  };
32
33
  export declare const isActiveDescendantAttribute = "data-is-active-descendant";
33
34
  export declare const activeDescendantActivatedDirectly = "activated-directly";
@@ -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;
@@ -129,8 +131,8 @@ const activeDescendantActivatedDirectly = 'activated-directly';
129
131
  const activeDescendantActivatedIndirectly = 'activated-indirectly';
130
132
  const hasActiveDescendantAttribute = 'data-has-active-descendant';
131
133
  function focusZone(container, settings) {
132
- var _a, _b, _c, _d, _e, _f;
133
- const focusableElements = [];
134
+ var _a, _b, _c, _d, _e, _f, _g;
135
+ const focusableElements = new indexedSet.IndexedSet();
134
136
  const savedTabIndex = new WeakMap();
135
137
  const bindKeys = (_a = settings === null || settings === void 0 ? void 0 : settings.bindKeys) !== null && _a !== void 0 ? _a : ((settings === null || settings === void 0 ? void 0 : settings.getNextFocusable) ? exports.FocusKeys.ArrowAll : exports.FocusKeys.ArrowVertical) | exports.FocusKeys.HomeAndEnd;
136
138
  const focusOutBehavior = (_b = settings === null || settings === void 0 ? void 0 : settings.focusOutBehavior) !== null && _b !== void 0 ? _b : 'stop';
@@ -138,11 +140,13 @@ function focusZone(container, settings) {
138
140
  const activeDescendantControl = settings === null || settings === void 0 ? void 0 : settings.activeDescendantControl;
139
141
  const activeDescendantCallback = settings === null || settings === void 0 ? void 0 : settings.onActiveDescendantChanged;
140
142
  const ignoreHoverEvents = (_d = settings === null || settings === void 0 ? void 0 : settings.ignoreHoverEvents) !== null && _d !== void 0 ? _d : false;
143
+ const focusPrependedElements = (_e = settings === null || settings === void 0 ? void 0 : settings.focusPrependedElements) !== null && _e !== void 0 ? _e : false;
141
144
  let currentFocusedElement;
142
- const preventScroll = (_e = settings === null || settings === void 0 ? void 0 : settings.preventScroll) !== null && _e !== void 0 ? _e : false;
145
+ let wasDirectlyActivated = false;
146
+ const preventScroll = (_f = settings === null || settings === void 0 ? void 0 : settings.preventScroll) !== null && _f !== void 0 ? _f : false;
143
147
  const preventInitialFocus = focusInStrategy === 'initial' && (settings === null || settings === void 0 ? void 0 : settings.activeDescendantControl);
144
148
  function getFirstFocusableElement() {
145
- return focusableElements[0];
149
+ return focusableElements.get(0);
146
150
  }
147
151
  function isActiveDescendantInputFocused() {
148
152
  return document.activeElement === activeDescendantControl;
@@ -150,6 +154,7 @@ function focusZone(container, settings) {
150
154
  function updateFocusedElement(to, directlyActivated = false) {
151
155
  const from = currentFocusedElement;
152
156
  currentFocusedElement = to;
157
+ wasDirectlyActivated = directlyActivated;
153
158
  if (activeDescendantControl) {
154
159
  if (to && isActiveDescendantInputFocused()) {
155
160
  setActiveDescendant(from, to, directlyActivated);
@@ -187,36 +192,41 @@ function focusZone(container, settings) {
187
192
  activeDescendantControl === null || activeDescendantControl === void 0 ? void 0 : activeDescendantControl.removeAttribute('aria-activedescendant');
188
193
  container.removeAttribute(hasActiveDescendantAttribute);
189
194
  previouslyActiveElement === null || previouslyActiveElement === void 0 ? void 0 : previouslyActiveElement.removeAttribute(isActiveDescendantAttribute);
190
- for (const item of container.querySelectorAll(`[${isActiveDescendantAttribute}]`)) {
191
- item === null || item === void 0 ? void 0 : item.removeAttribute(isActiveDescendantAttribute);
195
+ const items = container.querySelectorAll(`[${isActiveDescendantAttribute}]`);
196
+ for (let i = 0; i < items.length; i++) {
197
+ items[i].removeAttribute(isActiveDescendantAttribute);
192
198
  }
193
199
  activeDescendantCallback === null || activeDescendantCallback === void 0 ? void 0 : activeDescendantCallback(undefined, previouslyActiveElement, false);
194
200
  }
195
201
  function beginFocusManagement(...elements) {
196
- const filteredElements = 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; });
202
+ const filteredElements = (settings === null || settings === void 0 ? void 0 : settings.focusableElementFilter)
203
+ ? elements.filter(e => settings.focusableElementFilter(e))
204
+ : elements;
197
205
  if (filteredElements.length === 0) {
198
206
  return;
199
207
  }
200
- focusableElements.splice(findInsertionIndex(filteredElements), 0, ...filteredElements);
208
+ const insertionIndex = findInsertionIndex(filteredElements);
209
+ focusableElements.insertAt(insertionIndex, ...filteredElements);
201
210
  for (const element of filteredElements) {
202
211
  if (!savedTabIndex.has(element)) {
203
212
  savedTabIndex.set(element, element.getAttribute('tabindex'));
204
213
  }
205
214
  element.setAttribute('tabindex', '-1');
206
215
  }
207
- if (!currentFocusedElement && !preventInitialFocus) {
216
+ const shouldFocusPrepended = focusPrependedElements && insertionIndex === 0 && !wasDirectlyActivated;
217
+ if (!preventInitialFocus && (!currentFocusedElement || shouldFocusPrepended)) {
208
218
  updateFocusedElement(getFirstFocusableElement());
209
219
  }
210
220
  }
211
221
  function findInsertionIndex(elementsToInsert) {
212
222
  const firstElementToInsert = elementsToInsert[0];
213
- if (focusableElements.length === 0)
223
+ if (focusableElements.size === 0)
214
224
  return 0;
215
225
  let iMin = 0;
216
- let iMax = focusableElements.length - 1;
226
+ let iMax = focusableElements.size - 1;
217
227
  while (iMin <= iMax) {
218
228
  const i = Math.floor((iMin + iMax) / 2);
219
- const element = focusableElements[i];
229
+ const element = focusableElements.get(i);
220
230
  if (followsInDocument(firstElementToInsert, element)) {
221
231
  iMax = i - 1;
222
232
  }
@@ -231,10 +241,7 @@ function focusZone(container, settings) {
231
241
  }
232
242
  function endFocusManagement(...elements) {
233
243
  for (const element of elements) {
234
- const focusableElementIndex = focusableElements.indexOf(element);
235
- if (focusableElementIndex >= 0) {
236
- focusableElements.splice(focusableElementIndex, 1);
237
- }
244
+ focusableElements.delete(element);
238
245
  const savedIndex = savedTabIndex.get(element);
239
246
  if (savedIndex !== undefined) {
240
247
  if (savedIndex === null) {
@@ -261,29 +268,61 @@ function focusZone(container, settings) {
261
268
  if (!preventInitialFocus)
262
269
  updateFocusedElement(initialElement);
263
270
  const observer = new MutationObserver(mutations => {
271
+ const elementsToRemove = new Set();
272
+ const elementsToAdd = new Set();
273
+ const attributeRemovals = new Set();
274
+ const attributeAdditions = new Set();
264
275
  for (const mutation of mutations) {
265
- for (const removedNode of mutation.removedNodes) {
266
- if (removedNode instanceof HTMLElement) {
267
- endFocusManagement(...iterateFocusableElements.iterateFocusableElements(removedNode));
276
+ if (mutation.type === 'childList') {
277
+ for (const removedNode of mutation.removedNodes) {
278
+ if (removedNode instanceof HTMLElement) {
279
+ elementsToRemove.add(removedNode);
280
+ }
281
+ }
282
+ for (const addedNode of mutation.addedNodes) {
283
+ if (addedNode instanceof HTMLElement) {
284
+ elementsToAdd.add(addedNode);
285
+ }
268
286
  }
269
287
  }
270
- if (mutation.type === 'attributes' && mutation.oldValue === null) {
271
- if (mutation.target instanceof HTMLElement) {
272
- endFocusManagement(mutation.target);
288
+ else if (mutation.type === 'attributes' && mutation.target instanceof HTMLElement) {
289
+ const attributeName = mutation.attributeName;
290
+ const hasAttribute = attributeName ? mutation.target.hasAttribute(attributeName) : false;
291
+ if (hasAttribute) {
292
+ attributeRemovals.add(mutation.target);
293
+ }
294
+ else {
295
+ attributeAdditions.add(mutation.target);
273
296
  }
274
297
  }
275
298
  }
276
- for (const mutation of mutations) {
277
- for (const addedNode of mutation.addedNodes) {
278
- if (addedNode instanceof HTMLElement) {
279
- beginFocusManagement(...iterateFocusableElements.iterateFocusableElements(addedNode, iterateFocusableElementsOptions));
299
+ if (elementsToRemove.size > 0) {
300
+ const toRemove = [];
301
+ for (const node of elementsToRemove) {
302
+ for (const el of iterateFocusableElements.iterateFocusableElements(node)) {
303
+ toRemove.push(el);
280
304
  }
281
305
  }
282
- if (mutation.type === 'attributes' && mutation.oldValue !== null) {
283
- if (mutation.target instanceof HTMLElement) {
284
- beginFocusManagement(mutation.target);
306
+ if (toRemove.length > 0) {
307
+ endFocusManagement(...toRemove);
308
+ }
309
+ }
310
+ if (attributeRemovals.size > 0) {
311
+ endFocusManagement(...attributeRemovals);
312
+ }
313
+ if (elementsToAdd.size > 0) {
314
+ const toAdd = [];
315
+ for (const node of elementsToAdd) {
316
+ for (const el of iterateFocusableElements.iterateFocusableElements(node, iterateFocusableElementsOptions)) {
317
+ toAdd.push(el);
285
318
  }
286
319
  }
320
+ if (toAdd.length > 0) {
321
+ beginFocusManagement(...toAdd);
322
+ }
323
+ }
324
+ if (attributeAdditions.size > 0) {
325
+ beginFocusManagement(...attributeAdditions);
287
326
  }
288
327
  });
289
328
  observer.observe(container, {
@@ -293,9 +332,11 @@ function focusZone(container, settings) {
293
332
  attributeOldValue: true,
294
333
  });
295
334
  const controller = new AbortController();
296
- const signal = (_f = settings === null || settings === void 0 ? void 0 : settings.abortSignal) !== null && _f !== void 0 ? _f : controller.signal;
335
+ const signal = (_g = settings === null || settings === void 0 ? void 0 : settings.abortSignal) !== null && _g !== void 0 ? _g : controller.signal;
297
336
  signal.addEventListener('abort', () => {
337
+ observer.disconnect();
298
338
  endFocusManagement(...focusableElements);
339
+ focusableElements.clear();
299
340
  });
300
341
  let elementIndexFocusedByClick = undefined;
301
342
  container.addEventListener('mousedown', event => {
@@ -305,7 +346,7 @@ function focusZone(container, settings) {
305
346
  }, { signal });
306
347
  if (activeDescendantControl) {
307
348
  container.addEventListener('focusin', event => {
308
- if (event.target instanceof HTMLElement && focusableElements.includes(event.target)) {
349
+ if (event.target instanceof HTMLElement && focusableElements.has(event.target)) {
309
350
  activeDescendantControl.focus({ preventScroll });
310
351
  updateFocusedElement(event.target);
311
352
  }
@@ -315,6 +356,10 @@ function focusZone(container, settings) {
315
356
  if (!(target instanceof Node)) {
316
357
  return;
317
358
  }
359
+ if (target instanceof HTMLElement && focusableElements.has(target)) {
360
+ updateFocusedElement(target);
361
+ return;
362
+ }
318
363
  const focusableElement = focusableElements.find(element => element.contains(target));
319
364
  if (focusableElement) {
320
365
  updateFocusedElement(focusableElement);
@@ -339,8 +384,9 @@ function focusZone(container, settings) {
339
384
  if (event.target instanceof HTMLElement) {
340
385
  if (elementIndexFocusedByClick !== undefined) {
341
386
  if (elementIndexFocusedByClick >= 0) {
342
- if (focusableElements[elementIndexFocusedByClick] !== currentFocusedElement) {
343
- updateFocusedElement(focusableElements[elementIndexFocusedByClick]);
387
+ const clickedElement = focusableElements.get(elementIndexFocusedByClick);
388
+ if (clickedElement && clickedElement !== currentFocusedElement) {
389
+ updateFocusedElement(clickedElement);
344
390
  }
345
391
  }
346
392
  elementIndexFocusedByClick = undefined;
@@ -351,8 +397,8 @@ function focusZone(container, settings) {
351
397
  }
352
398
  else if (focusInStrategy === 'closest' || focusInStrategy === 'first') {
353
399
  if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
354
- const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.length - 1 : 0;
355
- const targetElement = focusableElements[targetElementIndex];
400
+ const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.size - 1 : 0;
401
+ const targetElement = focusableElements.get(targetElementIndex);
356
402
  targetElement === null || targetElement === void 0 ? void 0 : targetElement.focus({ preventScroll });
357
403
  return;
358
404
  }
@@ -363,8 +409,7 @@ function focusZone(container, settings) {
363
409
  else if (typeof focusInStrategy === 'function') {
364
410
  if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
365
411
  const elementToFocus = focusInStrategy(event.relatedTarget);
366
- const requestedFocusElementIndex = elementToFocus ? focusableElements.indexOf(elementToFocus) : -1;
367
- if (requestedFocusElementIndex >= 0 && elementToFocus instanceof HTMLElement) {
412
+ if (elementToFocus && focusableElements.has(elementToFocus)) {
368
413
  elementToFocus.focus({ preventScroll });
369
414
  return;
370
415
  }
@@ -423,26 +468,26 @@ function focusZone(container, settings) {
423
468
  nextFocusedIndex += 1;
424
469
  }
425
470
  else {
426
- nextFocusedIndex = focusableElements.length - 1;
471
+ nextFocusedIndex = focusableElements.size - 1;
427
472
  }
428
473
  if (nextFocusedIndex < 0) {
429
474
  if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
430
- nextFocusedIndex = focusableElements.length - 1;
475
+ nextFocusedIndex = focusableElements.size - 1;
431
476
  }
432
477
  else {
433
478
  nextFocusedIndex = 0;
434
479
  }
435
480
  }
436
- if (nextFocusedIndex >= focusableElements.length) {
481
+ if (nextFocusedIndex >= focusableElements.size) {
437
482
  if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
438
483
  nextFocusedIndex = 0;
439
484
  }
440
485
  else {
441
- nextFocusedIndex = focusableElements.length - 1;
486
+ nextFocusedIndex = focusableElements.size - 1;
442
487
  }
443
488
  }
444
489
  if (lastFocusedIndex !== nextFocusedIndex) {
445
- nextElementToFocus = focusableElements[nextFocusedIndex];
490
+ nextElementToFocus = focusableElements.get(nextFocusedIndex);
446
491
  }
447
492
  }
448
493
  if (activeDescendantControl) {
@@ -0,0 +1,13 @@
1
+ export declare class IndexedSet<T> {
2
+ private _items;
3
+ private _itemSet;
4
+ insertAt(index: number, ...elements: T[]): void;
5
+ delete(element: T): boolean;
6
+ has(element: T): boolean;
7
+ indexOf(element: T): number;
8
+ get(index: number): T | undefined;
9
+ get size(): number;
10
+ [Symbol.iterator](): Iterator<T>;
11
+ clear(): void;
12
+ find(predicate: (element: T) => boolean): T | undefined;
13
+ }
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ class IndexedSet {
4
+ constructor() {
5
+ this._items = [];
6
+ this._itemSet = new Set();
7
+ }
8
+ insertAt(index, ...elements) {
9
+ const newElements = elements.filter(e => !this._itemSet.has(e));
10
+ if (newElements.length === 0)
11
+ return;
12
+ this._items.splice(index, 0, ...newElements);
13
+ for (const element of newElements) {
14
+ this._itemSet.add(element);
15
+ }
16
+ }
17
+ delete(element) {
18
+ if (!this._itemSet.has(element))
19
+ return false;
20
+ const index = this._items.indexOf(element);
21
+ if (index >= 0) {
22
+ this._items.splice(index, 1);
23
+ }
24
+ this._itemSet.delete(element);
25
+ return true;
26
+ }
27
+ has(element) {
28
+ return this._itemSet.has(element);
29
+ }
30
+ indexOf(element) {
31
+ if (!this._itemSet.has(element))
32
+ return -1;
33
+ return this._items.indexOf(element);
34
+ }
35
+ get(index) {
36
+ return this._items[index];
37
+ }
38
+ get size() {
39
+ return this._items.length;
40
+ }
41
+ [Symbol.iterator]() {
42
+ return this._items[Symbol.iterator]();
43
+ }
44
+ clear() {
45
+ this._items = [];
46
+ this._itemSet.clear();
47
+ }
48
+ find(predicate) {
49
+ return this._items.find(predicate);
50
+ }
51
+ }
52
+
53
+ exports.IndexedSet = IndexedSet;
@@ -33,24 +33,32 @@ function* iterateFocusableElements(container, options = {}) {
33
33
  function getFocusableChild(container, lastChild = false) {
34
34
  return iterateFocusableElements(container, { reverse: lastChild, strict: true, onlyTabbable: true }).next().value;
35
35
  }
36
+ const DISABLEABLE_TAGS = new Set(['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTGROUP', 'OPTION', 'FIELDSET']);
36
37
  function isFocusable(elem, strict = false) {
37
- 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
  }
@@ -28,6 +28,7 @@ export type FocusZoneSettings = IterateFocusableElements & {
28
28
  focusInStrategy?: 'first' | 'closest' | 'previous' | 'initial' | ((previousFocusedElement: Element) => HTMLElement | undefined);
29
29
  preventScroll?: boolean;
30
30
  ignoreHoverEvents?: boolean;
31
+ focusPrependedElements?: boolean;
31
32
  };
32
33
  export declare const isActiveDescendantAttribute = "data-is-active-descendant";
33
34
  export declare const activeDescendantActivatedDirectly = "activated-directly";
@@ -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;
@@ -127,8 +129,8 @@ const activeDescendantActivatedDirectly = 'activated-directly';
127
129
  const activeDescendantActivatedIndirectly = 'activated-indirectly';
128
130
  const hasActiveDescendantAttribute = 'data-has-active-descendant';
129
131
  function focusZone(container, settings) {
130
- var _a, _b, _c, _d, _e, _f;
131
- const focusableElements = [];
132
+ var _a, _b, _c, _d, _e, _f, _g;
133
+ const focusableElements = new IndexedSet();
132
134
  const savedTabIndex = new WeakMap();
133
135
  const bindKeys = (_a = settings === null || settings === void 0 ? void 0 : settings.bindKeys) !== null && _a !== void 0 ? _a : ((settings === null || settings === void 0 ? void 0 : settings.getNextFocusable) ? FocusKeys.ArrowAll : FocusKeys.ArrowVertical) | FocusKeys.HomeAndEnd;
134
136
  const focusOutBehavior = (_b = settings === null || settings === void 0 ? void 0 : settings.focusOutBehavior) !== null && _b !== void 0 ? _b : 'stop';
@@ -136,11 +138,13 @@ function focusZone(container, settings) {
136
138
  const activeDescendantControl = settings === null || settings === void 0 ? void 0 : settings.activeDescendantControl;
137
139
  const activeDescendantCallback = settings === null || settings === void 0 ? void 0 : settings.onActiveDescendantChanged;
138
140
  const ignoreHoverEvents = (_d = settings === null || settings === void 0 ? void 0 : settings.ignoreHoverEvents) !== null && _d !== void 0 ? _d : false;
141
+ const focusPrependedElements = (_e = settings === null || settings === void 0 ? void 0 : settings.focusPrependedElements) !== null && _e !== void 0 ? _e : false;
139
142
  let currentFocusedElement;
140
- const preventScroll = (_e = settings === null || settings === void 0 ? void 0 : settings.preventScroll) !== null && _e !== void 0 ? _e : false;
143
+ let wasDirectlyActivated = false;
144
+ const preventScroll = (_f = settings === null || settings === void 0 ? void 0 : settings.preventScroll) !== null && _f !== void 0 ? _f : false;
141
145
  const preventInitialFocus = focusInStrategy === 'initial' && (settings === null || settings === void 0 ? void 0 : settings.activeDescendantControl);
142
146
  function getFirstFocusableElement() {
143
- return focusableElements[0];
147
+ return focusableElements.get(0);
144
148
  }
145
149
  function isActiveDescendantInputFocused() {
146
150
  return document.activeElement === activeDescendantControl;
@@ -148,6 +152,7 @@ function focusZone(container, settings) {
148
152
  function updateFocusedElement(to, directlyActivated = false) {
149
153
  const from = currentFocusedElement;
150
154
  currentFocusedElement = to;
155
+ wasDirectlyActivated = directlyActivated;
151
156
  if (activeDescendantControl) {
152
157
  if (to && isActiveDescendantInputFocused()) {
153
158
  setActiveDescendant(from, to, directlyActivated);
@@ -185,36 +190,41 @@ function focusZone(container, settings) {
185
190
  activeDescendantControl === null || activeDescendantControl === void 0 ? void 0 : activeDescendantControl.removeAttribute('aria-activedescendant');
186
191
  container.removeAttribute(hasActiveDescendantAttribute);
187
192
  previouslyActiveElement === null || previouslyActiveElement === void 0 ? void 0 : previouslyActiveElement.removeAttribute(isActiveDescendantAttribute);
188
- for (const item of container.querySelectorAll(`[${isActiveDescendantAttribute}]`)) {
189
- item === null || item === void 0 ? void 0 : item.removeAttribute(isActiveDescendantAttribute);
193
+ const items = container.querySelectorAll(`[${isActiveDescendantAttribute}]`);
194
+ for (let i = 0; i < items.length; i++) {
195
+ items[i].removeAttribute(isActiveDescendantAttribute);
190
196
  }
191
197
  activeDescendantCallback === null || activeDescendantCallback === void 0 ? void 0 : activeDescendantCallback(undefined, previouslyActiveElement, false);
192
198
  }
193
199
  function beginFocusManagement(...elements) {
194
- const filteredElements = 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; });
200
+ const filteredElements = (settings === null || settings === void 0 ? void 0 : settings.focusableElementFilter)
201
+ ? elements.filter(e => settings.focusableElementFilter(e))
202
+ : elements;
195
203
  if (filteredElements.length === 0) {
196
204
  return;
197
205
  }
198
- focusableElements.splice(findInsertionIndex(filteredElements), 0, ...filteredElements);
206
+ const insertionIndex = findInsertionIndex(filteredElements);
207
+ focusableElements.insertAt(insertionIndex, ...filteredElements);
199
208
  for (const element of filteredElements) {
200
209
  if (!savedTabIndex.has(element)) {
201
210
  savedTabIndex.set(element, element.getAttribute('tabindex'));
202
211
  }
203
212
  element.setAttribute('tabindex', '-1');
204
213
  }
205
- if (!currentFocusedElement && !preventInitialFocus) {
214
+ const shouldFocusPrepended = focusPrependedElements && insertionIndex === 0 && !wasDirectlyActivated;
215
+ if (!preventInitialFocus && (!currentFocusedElement || shouldFocusPrepended)) {
206
216
  updateFocusedElement(getFirstFocusableElement());
207
217
  }
208
218
  }
209
219
  function findInsertionIndex(elementsToInsert) {
210
220
  const firstElementToInsert = elementsToInsert[0];
211
- if (focusableElements.length === 0)
221
+ if (focusableElements.size === 0)
212
222
  return 0;
213
223
  let iMin = 0;
214
- let iMax = focusableElements.length - 1;
224
+ let iMax = focusableElements.size - 1;
215
225
  while (iMin <= iMax) {
216
226
  const i = Math.floor((iMin + iMax) / 2);
217
- const element = focusableElements[i];
227
+ const element = focusableElements.get(i);
218
228
  if (followsInDocument(firstElementToInsert, element)) {
219
229
  iMax = i - 1;
220
230
  }
@@ -229,10 +239,7 @@ function focusZone(container, settings) {
229
239
  }
230
240
  function endFocusManagement(...elements) {
231
241
  for (const element of elements) {
232
- const focusableElementIndex = focusableElements.indexOf(element);
233
- if (focusableElementIndex >= 0) {
234
- focusableElements.splice(focusableElementIndex, 1);
235
- }
242
+ focusableElements.delete(element);
236
243
  const savedIndex = savedTabIndex.get(element);
237
244
  if (savedIndex !== undefined) {
238
245
  if (savedIndex === null) {
@@ -259,29 +266,61 @@ function focusZone(container, settings) {
259
266
  if (!preventInitialFocus)
260
267
  updateFocusedElement(initialElement);
261
268
  const observer = new MutationObserver(mutations => {
269
+ const elementsToRemove = new Set();
270
+ const elementsToAdd = new Set();
271
+ const attributeRemovals = new Set();
272
+ const attributeAdditions = new Set();
262
273
  for (const mutation of mutations) {
263
- for (const removedNode of mutation.removedNodes) {
264
- if (removedNode instanceof HTMLElement) {
265
- endFocusManagement(...iterateFocusableElements(removedNode));
274
+ if (mutation.type === 'childList') {
275
+ for (const removedNode of mutation.removedNodes) {
276
+ if (removedNode instanceof HTMLElement) {
277
+ elementsToRemove.add(removedNode);
278
+ }
279
+ }
280
+ for (const addedNode of mutation.addedNodes) {
281
+ if (addedNode instanceof HTMLElement) {
282
+ elementsToAdd.add(addedNode);
283
+ }
266
284
  }
267
285
  }
268
- if (mutation.type === 'attributes' && mutation.oldValue === null) {
269
- if (mutation.target instanceof HTMLElement) {
270
- endFocusManagement(mutation.target);
286
+ else if (mutation.type === 'attributes' && mutation.target instanceof HTMLElement) {
287
+ const attributeName = mutation.attributeName;
288
+ const hasAttribute = attributeName ? mutation.target.hasAttribute(attributeName) : false;
289
+ if (hasAttribute) {
290
+ attributeRemovals.add(mutation.target);
291
+ }
292
+ else {
293
+ attributeAdditions.add(mutation.target);
271
294
  }
272
295
  }
273
296
  }
274
- for (const mutation of mutations) {
275
- for (const addedNode of mutation.addedNodes) {
276
- if (addedNode instanceof HTMLElement) {
277
- beginFocusManagement(...iterateFocusableElements(addedNode, iterateFocusableElementsOptions));
297
+ if (elementsToRemove.size > 0) {
298
+ const toRemove = [];
299
+ for (const node of elementsToRemove) {
300
+ for (const el of iterateFocusableElements(node)) {
301
+ toRemove.push(el);
278
302
  }
279
303
  }
280
- if (mutation.type === 'attributes' && mutation.oldValue !== null) {
281
- if (mutation.target instanceof HTMLElement) {
282
- beginFocusManagement(mutation.target);
304
+ if (toRemove.length > 0) {
305
+ endFocusManagement(...toRemove);
306
+ }
307
+ }
308
+ if (attributeRemovals.size > 0) {
309
+ endFocusManagement(...attributeRemovals);
310
+ }
311
+ if (elementsToAdd.size > 0) {
312
+ const toAdd = [];
313
+ for (const node of elementsToAdd) {
314
+ for (const el of iterateFocusableElements(node, iterateFocusableElementsOptions)) {
315
+ toAdd.push(el);
283
316
  }
284
317
  }
318
+ if (toAdd.length > 0) {
319
+ beginFocusManagement(...toAdd);
320
+ }
321
+ }
322
+ if (attributeAdditions.size > 0) {
323
+ beginFocusManagement(...attributeAdditions);
285
324
  }
286
325
  });
287
326
  observer.observe(container, {
@@ -291,9 +330,11 @@ function focusZone(container, settings) {
291
330
  attributeOldValue: true,
292
331
  });
293
332
  const controller = new AbortController();
294
- const signal = (_f = settings === null || settings === void 0 ? void 0 : settings.abortSignal) !== null && _f !== void 0 ? _f : controller.signal;
333
+ const signal = (_g = settings === null || settings === void 0 ? void 0 : settings.abortSignal) !== null && _g !== void 0 ? _g : controller.signal;
295
334
  signal.addEventListener('abort', () => {
335
+ observer.disconnect();
296
336
  endFocusManagement(...focusableElements);
337
+ focusableElements.clear();
297
338
  });
298
339
  let elementIndexFocusedByClick = undefined;
299
340
  container.addEventListener('mousedown', event => {
@@ -303,7 +344,7 @@ function focusZone(container, settings) {
303
344
  }, { signal });
304
345
  if (activeDescendantControl) {
305
346
  container.addEventListener('focusin', event => {
306
- if (event.target instanceof HTMLElement && focusableElements.includes(event.target)) {
347
+ if (event.target instanceof HTMLElement && focusableElements.has(event.target)) {
307
348
  activeDescendantControl.focus({ preventScroll });
308
349
  updateFocusedElement(event.target);
309
350
  }
@@ -313,6 +354,10 @@ function focusZone(container, settings) {
313
354
  if (!(target instanceof Node)) {
314
355
  return;
315
356
  }
357
+ if (target instanceof HTMLElement && focusableElements.has(target)) {
358
+ updateFocusedElement(target);
359
+ return;
360
+ }
316
361
  const focusableElement = focusableElements.find(element => element.contains(target));
317
362
  if (focusableElement) {
318
363
  updateFocusedElement(focusableElement);
@@ -337,8 +382,9 @@ function focusZone(container, settings) {
337
382
  if (event.target instanceof HTMLElement) {
338
383
  if (elementIndexFocusedByClick !== undefined) {
339
384
  if (elementIndexFocusedByClick >= 0) {
340
- if (focusableElements[elementIndexFocusedByClick] !== currentFocusedElement) {
341
- updateFocusedElement(focusableElements[elementIndexFocusedByClick]);
385
+ const clickedElement = focusableElements.get(elementIndexFocusedByClick);
386
+ if (clickedElement && clickedElement !== currentFocusedElement) {
387
+ updateFocusedElement(clickedElement);
342
388
  }
343
389
  }
344
390
  elementIndexFocusedByClick = undefined;
@@ -349,8 +395,8 @@ function focusZone(container, settings) {
349
395
  }
350
396
  else if (focusInStrategy === 'closest' || focusInStrategy === 'first') {
351
397
  if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
352
- const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.length - 1 : 0;
353
- const targetElement = focusableElements[targetElementIndex];
398
+ const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.size - 1 : 0;
399
+ const targetElement = focusableElements.get(targetElementIndex);
354
400
  targetElement === null || targetElement === void 0 ? void 0 : targetElement.focus({ preventScroll });
355
401
  return;
356
402
  }
@@ -361,8 +407,7 @@ function focusZone(container, settings) {
361
407
  else if (typeof focusInStrategy === 'function') {
362
408
  if (event.relatedTarget instanceof Element && !container.contains(event.relatedTarget)) {
363
409
  const elementToFocus = focusInStrategy(event.relatedTarget);
364
- const requestedFocusElementIndex = elementToFocus ? focusableElements.indexOf(elementToFocus) : -1;
365
- if (requestedFocusElementIndex >= 0 && elementToFocus instanceof HTMLElement) {
410
+ if (elementToFocus && focusableElements.has(elementToFocus)) {
366
411
  elementToFocus.focus({ preventScroll });
367
412
  return;
368
413
  }
@@ -421,26 +466,26 @@ function focusZone(container, settings) {
421
466
  nextFocusedIndex += 1;
422
467
  }
423
468
  else {
424
- nextFocusedIndex = focusableElements.length - 1;
469
+ nextFocusedIndex = focusableElements.size - 1;
425
470
  }
426
471
  if (nextFocusedIndex < 0) {
427
472
  if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
428
- nextFocusedIndex = focusableElements.length - 1;
473
+ nextFocusedIndex = focusableElements.size - 1;
429
474
  }
430
475
  else {
431
476
  nextFocusedIndex = 0;
432
477
  }
433
478
  }
434
- if (nextFocusedIndex >= focusableElements.length) {
479
+ if (nextFocusedIndex >= focusableElements.size) {
435
480
  if (focusOutBehavior === 'wrap' && event.key !== 'Tab') {
436
481
  nextFocusedIndex = 0;
437
482
  }
438
483
  else {
439
- nextFocusedIndex = focusableElements.length - 1;
484
+ nextFocusedIndex = focusableElements.size - 1;
440
485
  }
441
486
  }
442
487
  if (lastFocusedIndex !== nextFocusedIndex) {
443
- nextElementToFocus = focusableElements[nextFocusedIndex];
488
+ nextElementToFocus = focusableElements.get(nextFocusedIndex);
444
489
  }
445
490
  }
446
491
  if (activeDescendantControl) {
@@ -0,0 +1,13 @@
1
+ export declare class IndexedSet<T> {
2
+ private _items;
3
+ private _itemSet;
4
+ insertAt(index: number, ...elements: T[]): void;
5
+ delete(element: T): boolean;
6
+ has(element: T): boolean;
7
+ indexOf(element: T): number;
8
+ get(index: number): T | undefined;
9
+ get size(): number;
10
+ [Symbol.iterator](): Iterator<T>;
11
+ clear(): void;
12
+ find(predicate: (element: T) => boolean): T | undefined;
13
+ }
@@ -0,0 +1,51 @@
1
+ class IndexedSet {
2
+ constructor() {
3
+ this._items = [];
4
+ this._itemSet = new Set();
5
+ }
6
+ insertAt(index, ...elements) {
7
+ const newElements = elements.filter(e => !this._itemSet.has(e));
8
+ if (newElements.length === 0)
9
+ return;
10
+ this._items.splice(index, 0, ...newElements);
11
+ for (const element of newElements) {
12
+ this._itemSet.add(element);
13
+ }
14
+ }
15
+ delete(element) {
16
+ if (!this._itemSet.has(element))
17
+ return false;
18
+ const index = this._items.indexOf(element);
19
+ if (index >= 0) {
20
+ this._items.splice(index, 1);
21
+ }
22
+ this._itemSet.delete(element);
23
+ return true;
24
+ }
25
+ has(element) {
26
+ return this._itemSet.has(element);
27
+ }
28
+ indexOf(element) {
29
+ if (!this._itemSet.has(element))
30
+ return -1;
31
+ return this._items.indexOf(element);
32
+ }
33
+ get(index) {
34
+ return this._items[index];
35
+ }
36
+ get size() {
37
+ return this._items.length;
38
+ }
39
+ [Symbol.iterator]() {
40
+ return this._items[Symbol.iterator]();
41
+ }
42
+ clear() {
43
+ this._items = [];
44
+ this._itemSet.clear();
45
+ }
46
+ find(predicate) {
47
+ return this._items.find(predicate);
48
+ }
49
+ }
50
+
51
+ export { IndexedSet };
@@ -31,24 +31,32 @@ function* iterateFocusableElements(container, options = {}) {
31
31
  function getFocusableChild(container, lastChild = false) {
32
32
  return iterateFocusableElements(container, { reverse: lastChild, strict: true, onlyTabbable: true }).next().value;
33
33
  }
34
+ const DISABLEABLE_TAGS = new Set(['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTGROUP', 'OPTION', 'FIELDSET']);
34
35
  function isFocusable(elem, strict = false) {
35
- 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",
3
+ "version": "1.9.1-rc.69df32c",
4
4
  "description": "Shared behaviors for JavaScript components",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cjs/index.js",
@@ -82,7 +82,7 @@
82
82
  "@testing-library/react": "^16.0.0",
83
83
  "@testing-library/user-event": "^14.5.1",
84
84
  "@types/jest": "^30.0.0",
85
- "@types/node": "^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",