@mui/utils 9.0.0-beta.0 → 9.0.0

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.
@@ -6,61 +6,103 @@ var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWild
6
6
  Object.defineProperty(exports, "__esModule", {
7
7
  value: true
8
8
  });
9
- exports.default = useRovingTabIndex;
9
+ exports.isItemFocusable = isItemFocusable;
10
+ exports.useRovingTabIndexItem = useRovingTabIndexItem;
11
+ exports.useRovingTabIndexRoot = useRovingTabIndexRoot;
10
12
  var React = _interopRequireWildcard(require("react"));
11
- var _ownerDocument = _interopRequireDefault(require("../ownerDocument"));
13
+ var _fastObjectShallowCompare = _interopRequireDefault(require("../fastObjectShallowCompare"));
12
14
  var _getActiveElement = _interopRequireDefault(require("../getActiveElement"));
15
+ var _ownerDocument = _interopRequireDefault(require("../ownerDocument"));
16
+ var _setRef = _interopRequireDefault(require("../setRef"));
17
+ var _useEnhancedEffect = _interopRequireDefault(require("../useEnhancedEffect"));
18
+ var _useEventCallback = _interopRequireDefault(require("../useEventCallback"));
19
+ var _useForkRef = _interopRequireDefault(require("../useForkRef"));
20
+ var _RovingTabIndexContext = require("./RovingTabIndexContext");
13
21
  const SUPPORTED_KEYS = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
14
22
 
15
23
  /**
16
- * Provides roving tab index behavior for a container and its focusable children.
24
+ * Provides roving tab index behavior for a composite container and its focusable children.
17
25
  * This is useful for implementing keyboard navigation in components like menus, tabs, and lists.
18
26
  * The hook manages the focus state of child elements and provides props to be spread on both the container and the items.
19
27
  * The container will handle keyboard events to move focus between items based on the specified orientation and wrapping behavior.
20
- *
21
- * @param options - Configuration options for the roving tab index behavior, including orientation, initial focusable index, RTL support, and custom focus logic.
22
- * @returns An object containing `getItemProps` and `getContainerProps` functions to be spread on the respective elements, and a `focusNext` function to programmatically move focus to the next item.
23
28
  */
24
- function useRovingTabIndex(options) {
29
+ function useRovingTabIndexRoot(params) {
25
30
  const {
31
+ activeItemId: activeItemIdProp,
32
+ getDefaultActiveItemId,
26
33
  orientation,
27
- focusableIndex: focusableIndexProp,
28
34
  isRtl = false,
29
- shouldFocus = internalShouldFocus,
30
- shouldWrap = true
31
- } = options;
32
- const initialFocusableIndex = focusableIndexProp ?? 0;
33
- const [focusableIndex, setFocusableIndex] = React.useState(initialFocusableIndex);
34
- const elementsRef = React.useRef([]);
35
- const containerRef = React.useRef(null);
36
- const previousFocusableIndexPropRef = React.useRef(initialFocusableIndex);
37
- if (focusableIndexProp !== undefined && focusableIndexProp !== previousFocusableIndexPropRef.current) {
38
- previousFocusableIndexPropRef.current = focusableIndexProp;
39
- if (focusableIndexProp !== focusableIndex) {
40
- setFocusableIndex(focusableIndexProp);
35
+ isItemFocusable: itemFilter = isItemFocusable,
36
+ wrap = true
37
+ } = params;
38
+ const [activeItemIdState, setActiveItemIdState] = React.useState(activeItemIdProp);
39
+ const previousActiveItemIdPropRef = React.useRef(activeItemIdProp);
40
+ let activeItemIdCandidate = activeItemIdState;
41
+ if (activeItemIdProp !== previousActiveItemIdPropRef.current) {
42
+ previousActiveItemIdPropRef.current = activeItemIdProp;
43
+ if (activeItemIdProp !== undefined && activeItemIdProp !== activeItemIdState) {
44
+ activeItemIdCandidate = activeItemIdProp;
45
+ setActiveItemIdState(activeItemIdProp);
41
46
  }
42
47
  }
43
- React.useEffect(() => {
44
- if (elementsRef.current.length === 0 || focusableIndex === -1 || focusableIndex >= elementsRef.current.length) {
48
+ const containerRef = React.useRef(null);
49
+
50
+ // based on https://github.com/mui/base-ui/blob/7392a928fca91fcc68b9fad3439ac61e10f3f7ba/packages/react/src/composite/list/CompositeList.tsx#L25-L35
51
+ const itemMapRef = React.useRef(new Map());
52
+ const [mapTick, setMapTick] = React.useState(0);
53
+ const orderedItems = React.useMemo(() => {
54
+ void mapTick;
55
+ return getOrderedItems(itemMapRef.current);
56
+ }, [mapTick]);
57
+ const resolvedActiveItemId = resolveActiveItemId(activeItemIdCandidate, orderedItems, itemFilter, getDefaultActiveItemId);
58
+ const activeItemIdRef = React.useRef(resolvedActiveItemId);
59
+ activeItemIdRef.current = resolvedActiveItemId;
60
+ const getActiveItem = React.useCallback(() => {
61
+ const snapshot = getOrderedItems(itemMapRef.current);
62
+ const resolvedItemId = resolveActiveItemId(activeItemIdRef.current, snapshot, itemFilter, getDefaultActiveItemId);
63
+ return getItemById(snapshot, resolvedItemId);
64
+ }, [getDefaultActiveItemId, itemFilter]);
65
+ const getItemMap = React.useCallback(() => {
66
+ return itemMapRef.current;
67
+ }, []);
68
+ const registerItem = (0, _useEventCallback.default)(item => {
69
+ const previousItem = itemMapRef.current.get(item.id);
70
+ if ((0, _fastObjectShallowCompare.default)(previousItem ?? null, item)) {
45
71
  return;
46
72
  }
47
- if (!shouldFocus(elementsRef.current[focusableIndex])) {
48
- const nextIndex = focusNext(elementsRef, focusableIndex, 'next', false, shouldFocus);
49
- setFocusableIndex(nextIndex);
73
+ itemMapRef.current.set(item.id, item);
74
+ setMapTick(value => value + 1);
75
+ });
76
+ const unregisterItem = (0, _useEventCallback.default)(itemId => {
77
+ if (itemMapRef.current.delete(itemId)) {
78
+ setMapTick(value => value + 1);
79
+ }
80
+ });
81
+ const setActiveItemId = (0, _useEventCallback.default)(itemId => {
82
+ setActiveItemIdState(itemId);
83
+ });
84
+ const isItemActive = React.useCallback(itemId => {
85
+ return activeItemIdRef.current === itemId;
86
+ }, []);
87
+
88
+ // Moves focus relative to a starting index. This is the directional helper used by
89
+ // keyboard navigation and `focusNext()`.
90
+ const focusItem = React.useCallback((currentIndex, direction, wrap, isItemFocusableOverride) => {
91
+ const snapshot = getNavigableItemsSnapshot(itemMapRef.current);
92
+ const nextItem = getNextActiveItem(snapshot, currentIndex, direction, wrap, isItemFocusableOverride ?? itemFilter);
93
+ if (!nextItem) {
94
+ return null;
50
95
  }
51
- }, [focusableIndex, shouldFocus]);
52
- const getItemProps = React.useCallback((index, ref) => ({
53
- ref: handleRefs(ref, elementNode => {
54
- elementsRef.current[index] = elementNode;
55
- }),
56
- tabIndex: index === focusableIndex ? 0 : -1
57
- }), [focusableIndex]);
96
+ nextItem.element?.focus();
97
+ setActiveItemIdState(nextItem.id);
98
+ return nextItem;
99
+ }, [itemFilter]);
58
100
  const getContainerProps = React.useCallback(ref => {
59
101
  const onFocus = event => {
60
- const focusedElement = event.target;
61
- const focusedIndex = elementsRef.current.findIndex(element => element === focusedElement);
102
+ const snapshot = getNavigableItemsSnapshot(itemMapRef.current);
103
+ const focusedIndex = findItemIndexByElement(snapshot, event.target);
62
104
  if (focusedIndex !== -1) {
63
- setFocusableIndex(focusedIndex);
105
+ setActiveItemIdState(snapshot[focusedIndex].id);
64
106
  }
65
107
  };
66
108
  const onKeyDown = event => {
@@ -73,45 +115,42 @@ function useRovingTabIndex(options) {
73
115
  let previousItemKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
74
116
  let nextItemKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
75
117
  if (orientation === 'horizontal' && isRtl) {
76
- // swap previousItemKey with nextItemKey
77
118
  previousItemKey = 'ArrowRight';
78
119
  nextItemKey = 'ArrowLeft';
79
120
  }
121
+ const snapshot = getNavigableItemsSnapshot(itemMapRef.current);
80
122
  const currentFocus = (0, _getActiveElement.default)((0, _ownerDocument.default)(containerRef.current));
81
123
  const isFocusOnContainer = currentFocus === containerRef.current;
124
+ let currentIndex = getCurrentActiveItemIndex(snapshot, currentFocus, activeItemIdRef.current);
82
125
  let direction = 'next';
83
- let currentIndex = focusableIndex;
84
126
  switch (event.key) {
85
127
  case previousItemKey:
86
128
  direction = 'previous';
87
129
  event.preventDefault();
88
130
  if (isFocusOnContainer) {
89
131
  // Set to length, so that the previous focused element will be the last one.
90
- currentIndex = elementsRef.current.length;
132
+ currentIndex = snapshot.length;
91
133
  }
92
134
  break;
93
135
  case nextItemKey:
94
136
  event.preventDefault();
95
137
  if (isFocusOnContainer) {
96
- // Set to -1, so that the next focused element will be the first one.
97
138
  currentIndex = -1;
98
139
  }
99
140
  break;
100
141
  case 'Home':
101
142
  event.preventDefault();
102
- // Set to -1, so that the next focused element will be the first one.
103
143
  currentIndex = -1;
104
144
  break;
105
145
  case 'End':
106
146
  event.preventDefault();
107
147
  direction = 'previous';
108
- // Set to length, so that the previous focused element will be the last one.
109
- currentIndex = elementsRef.current.length;
148
+ currentIndex = snapshot.length;
110
149
  break;
111
150
  default:
112
151
  return;
113
152
  }
114
- focusNext(elementsRef, currentIndex, direction, shouldWrap, shouldFocus);
153
+ focusItem(currentIndex, direction, wrap);
115
154
  };
116
155
  return {
117
156
  onFocus,
@@ -120,51 +159,269 @@ function useRovingTabIndex(options) {
120
159
  containerRef.current = elementNode;
121
160
  })
122
161
  };
123
- }, [focusableIndex, isRtl, orientation, shouldWrap, shouldFocus]);
124
- const focusNextExport = React.useCallback(shouldFocusOverride => {
162
+ }, [focusItem, isRtl, orientation, wrap]);
163
+ const focusNext = React.useCallback(isItemFocusableOverride => {
164
+ const snapshot = getNavigableItemsSnapshot(itemMapRef.current);
125
165
  const currentFocus = (0, _getActiveElement.default)((0, _ownerDocument.default)(containerRef.current));
126
166
  const isFocusOnContainer = currentFocus === containerRef.current;
127
- let currentIndex = focusableIndex;
128
- if (isFocusOnContainer) {
129
- currentIndex = -1;
167
+ const currentIndex = isFocusOnContainer ? -1 : getCurrentActiveItemIndex(snapshot, currentFocus, activeItemIdRef.current);
168
+ return focusItem(currentIndex, 'next', true, isItemFocusableOverride)?.id ?? null;
169
+ }, [focusItem]);
170
+ return React.useMemo(() => ({
171
+ activeItemId: resolvedActiveItemId,
172
+ focusNext,
173
+ getActiveItem,
174
+ getContainerProps,
175
+ getItemMap,
176
+ isItemActive,
177
+ registerItem,
178
+ setActiveItemId,
179
+ unregisterItem
180
+ }), [resolvedActiveItemId, focusNext, getActiveItem, getContainerProps, getItemMap, isItemActive, registerItem, setActiveItemId, unregisterItem]);
181
+ }
182
+ function useRovingTabIndexItem(params) {
183
+ const rootContext = (0, _RovingTabIndexContext.useRovingTabIndexContext)();
184
+ const {
185
+ activeItemId,
186
+ registerItem,
187
+ unregisterItem
188
+ } = rootContext;
189
+ const elementRef = React.useRef(null);
190
+ const item = React.useMemo(() => ({
191
+ disabled: params.disabled ?? false,
192
+ element: null,
193
+ focusableWhenDisabled: params.focusableWhenDisabled ?? false,
194
+ id: params.id,
195
+ selected: params.selected ?? false,
196
+ textValue: params.textValue
197
+ }), [params.disabled, params.focusableWhenDisabled, params.id, params.selected, params.textValue]);
198
+ const latestItemRef = React.useRef(item);
199
+ // Keep the ref callback stable across item prop changes. The callback reads the latest
200
+ // item metadata from this ref so React does not have to detach and re-attach the ref
201
+ // every time `disabled`, `selected`, or similar item state changes.
202
+ latestItemRef.current = item;
203
+ const handleElementRef = React.useCallback(element => {
204
+ elementRef.current = element;
205
+ if (element == null) {
206
+ // Ref detachment runs during React's commit phase. Calling `unregisterItem()`
207
+ // synchronously here can trigger a nested state update while React is still
208
+ // finishing that commit. Unregister in a microtask so it runs after the
209
+ // commit completes.
210
+ queueMicrotask(() => {
211
+ // null check prevents stale unregisters for a remove-then-re-add edge case
212
+ if (elementRef.current == null) {
213
+ unregisterItem(params.id);
214
+ }
215
+ });
216
+ return;
130
217
  }
131
- const nextIndex = focusNext(elementsRef, currentIndex, 'next', true, shouldFocusOverride ?? shouldFocus);
132
- if (nextIndex !== -1) {
133
- setFocusableIndex(nextIndex);
218
+ registerItem({
219
+ ...latestItemRef.current,
220
+ element
221
+ });
222
+ }, [params.id, registerItem, unregisterItem]);
223
+
224
+ // `UseRovingTabIndexItemReturnValue.ref` must always be a callback ref. `useForkRef()`
225
+ // is typed to return `null` when every input ref is nullish, but this call always includes
226
+ // `handleElementRef`, so the merged ref cannot be `null` here.
227
+ const mergedRef = (0, _useForkRef.default)(params.ref, handleElementRef);
228
+ (0, _useEnhancedEffect.default)(() => {
229
+ if (!elementRef.current) {
230
+ return;
134
231
  }
135
- return nextIndex;
136
- }, [focusableIndex, shouldFocus]);
232
+ registerItem({
233
+ ...item,
234
+ element: elementRef.current
235
+ });
236
+ }, [item, registerItem]);
237
+ (0, _useEnhancedEffect.default)(() => {
238
+ const itemId = params.id;
239
+
240
+ // Keep unmount cleanup separate from the effect above. The effect above re-runs when
241
+ // item metadata changes, but we only want to unregister on unmount or when the item id changes.
242
+ return () => {
243
+ unregisterItem(itemId);
244
+ };
245
+ }, [params.id, unregisterItem]);
137
246
  return {
138
- getItemProps,
139
- getContainerProps,
140
- focusNext: focusNextExport
247
+ ref: mergedRef,
248
+ tabIndex: activeItemId === params.id ? 0 : -1
141
249
  };
142
250
  }
143
- function focusNext(elementsRef, currentIndex, direction, wrap, shouldFocus) {
144
- const lastIndex = elementsRef.current.length - 1;
251
+
252
+ /**
253
+ * Resolves which item id should own the roving tab stop for the current render.
254
+ *
255
+ * This is the top-level decision point for "who gets `tabIndex=0` right now?".
256
+ * For example:
257
+ * - `Tabs` sometimes passes `selectedValue` as `activeItemId` so the selected tab becomes
258
+ * the tab stop when focus enters the list from outside.
259
+ * - `MenuList` leaves `activeItemId` undefined and relies on the default-item logic below
260
+ * so that menu-specific rules decide which menu item should initially own the tab stop.
261
+ *
262
+ * @param activeItemId The item id supplied through the root hook's `activeItemId` option.
263
+ * `undefined` means "the caller did not ask for a specific item, use the default-item
264
+ * logic instead". `null` means "there is intentionally no preferred item, so also fall
265
+ * back to the default-item logic".
266
+ * @param items The ordered registered items currently in the roving set.
267
+ * @param isFocusable A predicate that decides whether an item may receive roving focus.
268
+ * @param getDefaultActiveItemId Optional caller-provided function that picks the preferred
269
+ * default item when `activeItemId` is not driving the tab stop directly.
270
+ * @returns The id of the item that should own `tabIndex=0`, or `null` if no item is focusable.
271
+ */
272
+ function resolveActiveItemId(activeItemId, items, isFocusable, getDefaultActiveItemId) {
273
+ if (activeItemId != null) {
274
+ return resolveRequestedItemId(activeItemId, items, isFocusable);
275
+ }
276
+ return resolveDefaultItemId(items, isFocusable, getDefaultActiveItemId);
277
+ }
278
+
279
+ /**
280
+ * Resolves the item id supplied through the root hook's `activeItemId` option.
281
+ *
282
+ * This path is used when a component such as `Tabs` or `MenuList` wants roving focus to
283
+ * follow a specific logical item. For example, `Tabs` can pass the selected tab's value as
284
+ * `activeItemId` so that the selected tab owns `tabIndex=0` when focus enters the list.
285
+ *
286
+ * @param requestedItemId The item id passed to the root hook's `activeItemId` option.
287
+ * @param items The ordered registered items currently in the roving set.
288
+ * @param isFocusable A predicate that decides whether an item may receive roving focus.
289
+ * @returns The same id when it still points to a focusable item. If that id no longer exists,
290
+ * returns the first focusable item. If the id still exists but the item is not focusable,
291
+ * returns the next focusable item after it without wrapping.
292
+ */
293
+ function resolveRequestedItemId(requestedItemId, items, isFocusable) {
294
+ const requestedItemIndex = findItemIndexById(items, requestedItemId);
295
+ if (requestedItemIndex === -1) {
296
+ return getFirstFocusableItemId(items, isFocusable);
297
+ }
298
+ if (isFocusable(items[requestedItemIndex])) {
299
+ return items[requestedItemIndex].id;
300
+ }
301
+ return getNextActiveItem(items, requestedItemIndex, 'next', false, isFocusable)?.id ?? null;
302
+ }
303
+
304
+ /**
305
+ * Resolves the default active item when the caller is not driving roving focus with
306
+ * `activeItemId`.
307
+ *
308
+ * This path is used on the initial render and whenever the caller leaves the choice of tab
309
+ * stop to the hook. `getDefaultActiveItemId` lets a component prefer a specific logical item
310
+ * before falling back to the first focusable item.
311
+ *
312
+ * For example:
313
+ * - `MenuList` uses this path all the time. When `variant="selectedMenu"`, it prefers the
314
+ * selected menu item; otherwise it prefers the first focusable menu item.
315
+ * - `Tabs` uses this path while focus is already inside the tab list, because at that point
316
+ * the current roving position should be driven by actual focus movement rather than by the
317
+ * selected tab value.
318
+ *
319
+ * @param items The ordered registered items currently in the roving set.
320
+ * @param isFocusable A predicate that decides whether an item may receive roving focus.
321
+ * @param getDefaultActiveItemId Optional caller-provided function that chooses which item
322
+ * should own the tab stop before the generic "first focusable item" fallback runs.
323
+ * @returns The default item id when it points to a focusable item, otherwise the first
324
+ * focusable item in the snapshot, or `null` when none are focusable.
325
+ */
326
+ function resolveDefaultItemId(items, isFocusable, getDefaultActiveItemId) {
327
+ const defaultItemId = getDefaultActiveItemId?.(items);
328
+ if (defaultItemId != null) {
329
+ const defaultItem = getItemById(items, defaultItemId);
330
+ if (defaultItem && isFocusable(defaultItem)) {
331
+ return defaultItem.id;
332
+ }
333
+ }
334
+ return getFirstFocusableItemId(items, isFocusable);
335
+ }
336
+
337
+ /**
338
+ * Finds the best starting index for keyboard navigation.
339
+ *
340
+ * This is used immediately before keyboard navigation and `focusNext()` navigation. It prefers
341
+ * the item that currently holds DOM focus, but if focus is on the container or outside the item
342
+ * set it falls back to the last known active item id.
343
+ *
344
+ * @param items The navigable item snapshot used for the current keyboard interaction.
345
+ * @param currentFocus The element that currently has DOM focus, if any.
346
+ * @param fallbackActiveItemId The last known active item id when focus is not on an item.
347
+ * @returns The focused item's index when focus is currently on an item. Otherwise, the index
348
+ * of the fallback active item id, or `-1` when no matching item exists.
349
+ */
350
+ function getCurrentActiveItemIndex(items, currentFocus, fallbackActiveItemId) {
351
+ if (currentFocus) {
352
+ const focusedIndex = findItemIndexByElement(items, currentFocus);
353
+ if (focusedIndex !== -1) {
354
+ return focusedIndex;
355
+ }
356
+ }
357
+ return findItemIndexById(items, fallbackActiveItemId);
358
+ }
359
+
360
+ /**
361
+ * Walks the item snapshot to find the next focusable item in the requested direction.
362
+ *
363
+ * This is the shared navigation primitive used by keyboard handling and imperative helpers
364
+ * such as `focusNext()`. It starts from the supplied index, advances through the snapshot in
365
+ * the requested direction, and skips over items that fail the `isFocusable` predicate.
366
+ *
367
+ * @param items The ordered navigable item snapshot.
368
+ * @param currentIndex The index to start from. Use `-1` to start before the first item or
369
+ * `items.length` to start after the last item.
370
+ * @param direction The direction to move through the snapshot.
371
+ * @param wrap Whether navigation should wrap around at the ends of the list.
372
+ * @param isFocusable A predicate that decides whether an item may receive roving focus.
373
+ * @returns The next focusable item record, or `null` when no focusable item can be reached.
374
+ */
375
+ function getNextActiveItem(items, currentIndex, direction, wrap, isFocusable) {
376
+ const lastIndex = items.length - 1;
377
+ if (lastIndex === -1) {
378
+ return null;
379
+ }
145
380
  let wrappedOnce = false;
146
381
  let nextIndex = getNextIndex(currentIndex, lastIndex, direction, wrap);
147
382
  const startIndex = nextIndex;
148
383
  while (nextIndex !== -1) {
149
- // Prevent infinite loop.
150
384
  if (nextIndex === startIndex) {
151
385
  if (wrappedOnce) {
152
- return -1;
386
+ return null;
153
387
  }
154
388
  wrappedOnce = true;
155
389
  }
156
- const nextElement = elementsRef.current[nextIndex];
157
-
158
- // Same logic as useAutocomplete.js
159
- if (!shouldFocus(nextElement)) {
160
- // Move to the next element.
390
+ const nextItem = items[nextIndex];
391
+ if (!nextItem || !isFocusable(nextItem)) {
161
392
  nextIndex = getNextIndex(nextIndex, lastIndex, direction, wrap);
162
393
  } else {
163
- nextElement?.focus();
164
- return nextIndex;
394
+ return nextItem;
165
395
  }
166
396
  }
167
- return -1;
397
+ return null;
398
+ }
399
+ function getFirstFocusableItemId(items, isFocusable) {
400
+ return items.find(item => isFocusable(item))?.id ?? null;
401
+ }
402
+ function getItemById(items, itemId) {
403
+ return itemId == null ? null : items.find(item => item.id === itemId) ?? null;
404
+ }
405
+ function findItemIndexById(items, itemId) {
406
+ return itemId == null ? -1 : items.findIndex(item => item.id === itemId);
407
+ }
408
+ function findItemIndexByElement(items, element) {
409
+ if (!element) {
410
+ return -1;
411
+ }
412
+ return items.findIndex(item => item.element === element || item.element?.contains(element));
413
+ }
414
+ function getOrderedItems(itemMap) {
415
+ const items = Array.from(itemMap.values());
416
+ if (items.every(item => item.element == null)) {
417
+ return items;
418
+ }
419
+ const connectedItems = items.filter(isConnectedItem).sort((itemA, itemB) => sortByDocumentPosition(itemA.element, itemB.element));
420
+ const disconnectedItems = items.filter(item => !isConnectedItem(item));
421
+ return [...connectedItems, ...disconnectedItems];
422
+ }
423
+ function getNavigableItemsSnapshot(itemMap) {
424
+ return getOrderedItems(itemMap).filter(isConnectedItem);
168
425
  }
169
426
  function getNextIndex(currentIndex, lastIndex, direction, wrap = true) {
170
427
  if (direction === 'next') {
@@ -178,20 +435,39 @@ function getNextIndex(currentIndex, lastIndex, direction, wrap = true) {
178
435
  }
179
436
  return currentIndex - 1;
180
437
  }
181
- function internalShouldFocus(element) {
182
- if (!element) {
438
+ function isItemFocusable(item) {
439
+ if (!item.element) {
183
440
  return false;
184
441
  }
185
- return !element.hasAttribute('disabled') && element.getAttribute('aria-disabled') !== 'true' && element.hasAttribute('tabindex');
442
+ if (item.focusableWhenDisabled) {
443
+ return true;
444
+ }
445
+ return !item.disabled && !item.element.hasAttribute('disabled') && item.element.getAttribute('aria-disabled') !== 'true' && item.element.hasAttribute('tabindex');
446
+ }
447
+ function isConnectedItem(item) {
448
+ return item.element != null && item.element.isConnected;
186
449
  }
450
+
451
+ /* eslint-disable no-bitwise */
452
+ function sortByDocumentPosition(a, b) {
453
+ if (a === b) {
454
+ return 0;
455
+ }
456
+ const position = a.compareDocumentPosition(b);
457
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING || position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
458
+ return -1;
459
+ }
460
+ if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) {
461
+ return 1;
462
+ }
463
+ return 0;
464
+ }
465
+ /* eslint-enable no-bitwise */
466
+
187
467
  function handleRefs(...refs) {
188
468
  return node => {
189
469
  refs.forEach(ref => {
190
- if (typeof ref === 'function') {
191
- ref(node);
192
- } else if (ref) {
193
- ref.current = node;
194
- }
470
+ (0, _setRef.default)(ref ?? null, node);
195
471
  });
196
472
  };
197
473
  }