@octanejs/base-ui 0.1.15 → 0.1.17

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.
@@ -0,0 +1,437 @@
1
+ // The two-dimensional half of `.base-ui/packages/react/src/floating-ui-react/utils/composite.ts`
2
+ // (v1.6.0): the grid navigation algorithm and the cell-map helpers that let a spanning or densely
3
+ // packed item occupy more than one cell. Both `gridNavigation` entry points
4
+ // (`../floating/gridNavigation`, `./gridNavigation`) build on these, and nothing else does — a
5
+ // composite that never passes `grid` leaves this module out of its bundle entirely, which is the
6
+ // point of the injection.
7
+ //
8
+ // Upstream keeps this in the same module as the one-dimensional list algebra; this binding already
9
+ // vendors that into `./list-utils`, so the grid half sits beside it rather than folding back in.
10
+ // That also keeps `list-utils` free of a cycle: it imports `isElementVisible` from
11
+ // `../floating/composite`, and nothing imports this file back.
12
+ //
13
+ // octane adaptations: `React.KeyboardEvent` is the NATIVE `KeyboardEvent`.
14
+ // `createGridCellMap`'s invalid-width check is dev-only upstream; it throws unconditionally here,
15
+ // because this binding carries no `process.env.NODE_ENV` branches and the production fallthrough
16
+ // upstream accepts is an infinite placement loop, not a degraded layout.
17
+ import { floor, type Dimensions } from '@floating-ui/utils';
18
+
19
+ import { ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP } from './keys';
20
+ import {
21
+ findNonDisabledListIndex,
22
+ isDifferentGridRow,
23
+ isIndexOutOfListBounds,
24
+ isListIndexDisabled,
25
+ stopEvent,
26
+ type DisabledIndices,
27
+ } from './list-utils';
28
+
29
+ export function getGridNavigatedIndex(
30
+ list: Array<HTMLElement | null>,
31
+ {
32
+ event,
33
+ orientation,
34
+ loopFocus,
35
+ onLoop,
36
+ rtl,
37
+ cols,
38
+ disabledIndices,
39
+ minIndex,
40
+ maxIndex,
41
+ prevIndex,
42
+ stopEvent: stop = false,
43
+ }: {
44
+ event: KeyboardEvent;
45
+ orientation: 'horizontal' | 'vertical' | 'both';
46
+ loopFocus: boolean;
47
+ onLoop?: ((event: KeyboardEvent, prevIndex: number, nextIndex: number) => number) | undefined;
48
+ rtl: boolean;
49
+ cols: number;
50
+ disabledIndices: DisabledIndices | undefined;
51
+ minIndex: number;
52
+ maxIndex: number;
53
+ prevIndex: number;
54
+ stopEvent?: boolean | undefined;
55
+ },
56
+ ): number {
57
+ let nextIndex = prevIndex;
58
+
59
+ let verticalDirection: 'up' | 'down' | undefined;
60
+ if (event.key === ARROW_UP) {
61
+ verticalDirection = 'up';
62
+ } else if (event.key === ARROW_DOWN) {
63
+ verticalDirection = 'down';
64
+ }
65
+
66
+ if (verticalDirection) {
67
+ // -------------------------------------------------------------------------
68
+ // Detect row structure only when handling vertical navigation. This keeps
69
+ // the non-vertical key paths free from row inference work.
70
+ // -------------------------------------------------------------------------
71
+ const rows: number[][] = [];
72
+ const rowIndexMap: number[] = [];
73
+ let hasRoleRow = false;
74
+ let visibleItemCount = 0;
75
+ {
76
+ let currentRowEl: Element | null = null;
77
+ let currentRowIndex = -1;
78
+
79
+ list.forEach((el, idx) => {
80
+ if (el == null) {
81
+ return;
82
+ }
83
+
84
+ visibleItemCount += 1;
85
+
86
+ const rowEl = el.closest('[role="row"]');
87
+ if (rowEl) {
88
+ hasRoleRow = true;
89
+ }
90
+
91
+ if (rowEl !== currentRowEl || currentRowIndex === -1) {
92
+ currentRowEl = rowEl;
93
+ currentRowIndex += 1;
94
+ rows[currentRowIndex] = [];
95
+ }
96
+ rows[currentRowIndex].push(idx);
97
+ rowIndexMap[idx] = currentRowIndex;
98
+ });
99
+ }
100
+
101
+ let hasDomRows = false;
102
+ let inferredDomCols = 0;
103
+
104
+ if (hasRoleRow) {
105
+ for (const row of rows) {
106
+ const rowLength = row.length;
107
+
108
+ if (rowLength > inferredDomCols) {
109
+ inferredDomCols = rowLength;
110
+ }
111
+
112
+ if (rowLength !== cols) {
113
+ hasDomRows = true;
114
+ }
115
+ }
116
+ }
117
+
118
+ const hasVirtualizedGaps = hasDomRows && visibleItemCount < list.length;
119
+ const verticalCols = inferredDomCols || cols;
120
+
121
+ const navigateVertically = (direction: 'up' | 'down') => {
122
+ if (!hasDomRows || prevIndex === -1) {
123
+ return undefined;
124
+ }
125
+
126
+ const currentRow = rowIndexMap[prevIndex];
127
+ if (currentRow == null) {
128
+ return undefined;
129
+ }
130
+
131
+ const colInRow = rows[currentRow].indexOf(prevIndex);
132
+ const step = direction === 'up' ? -1 : 1;
133
+
134
+ for (let nextRow = currentRow + step, i = 0; i < rows.length; i += 1, nextRow += step) {
135
+ if (nextRow < 0 || nextRow >= rows.length) {
136
+ if (!loopFocus || hasVirtualizedGaps) {
137
+ return undefined;
138
+ }
139
+ nextRow = nextRow < 0 ? rows.length - 1 : 0;
140
+ if (onLoop) {
141
+ const clampedCol = Math.min(colInRow, rows[nextRow].length - 1);
142
+ const targetItemIndex = rows[nextRow][clampedCol] ?? rows[nextRow][0];
143
+ const returnedItemIndex = onLoop(event, prevIndex, targetItemIndex);
144
+ nextRow = rowIndexMap[returnedItemIndex] ?? nextRow;
145
+ }
146
+ }
147
+
148
+ const targetRow = rows[nextRow];
149
+ for (let col = Math.min(colInRow, targetRow.length - 1); col >= 0; col -= 1) {
150
+ const candidate = targetRow[col];
151
+ if (!isListIndexDisabled(list, candidate, disabledIndices)) {
152
+ return candidate;
153
+ }
154
+ }
155
+ }
156
+
157
+ return undefined;
158
+ };
159
+
160
+ const navigateVerticallyWithInferredRows = (direction: 'up' | 'down') => {
161
+ if (!hasVirtualizedGaps || prevIndex === -1) {
162
+ return undefined;
163
+ }
164
+
165
+ const colInRow = prevIndex % verticalCols;
166
+ const rowStep = direction === 'up' ? -verticalCols : verticalCols;
167
+ const lastRowStart = maxIndex - (maxIndex % verticalCols);
168
+ const rowCount = floor(maxIndex / verticalCols) + 1;
169
+
170
+ for (
171
+ let rowStart = prevIndex - colInRow + rowStep, i = 0;
172
+ i < rowCount;
173
+ i += 1, rowStart += rowStep
174
+ ) {
175
+ if (rowStart < 0 || rowStart > maxIndex) {
176
+ if (!loopFocus) {
177
+ return undefined;
178
+ }
179
+ rowStart = rowStart < 0 ? lastRowStart : 0;
180
+ }
181
+
182
+ const rowEnd = Math.min(rowStart + verticalCols - 1, maxIndex);
183
+ for (
184
+ let candidate = Math.min(rowStart + colInRow, rowEnd);
185
+ candidate >= rowStart;
186
+ candidate -= 1
187
+ ) {
188
+ if (!isListIndexDisabled(list, candidate, disabledIndices)) {
189
+ return candidate;
190
+ }
191
+ }
192
+ }
193
+
194
+ return undefined;
195
+ };
196
+
197
+ if (stop) {
198
+ stopEvent(event);
199
+ }
200
+
201
+ const verticalCandidate =
202
+ navigateVertically(verticalDirection) ??
203
+ navigateVerticallyWithInferredRows(verticalDirection);
204
+
205
+ if (verticalCandidate !== undefined) {
206
+ nextIndex = verticalCandidate;
207
+ } else if (prevIndex === -1) {
208
+ nextIndex = verticalDirection === 'up' ? maxIndex : minIndex;
209
+ } else {
210
+ nextIndex = findNonDisabledListIndex(list, {
211
+ startingIndex: prevIndex,
212
+ amount: verticalCols,
213
+ decrement: verticalDirection === 'up',
214
+ disabledIndices,
215
+ });
216
+
217
+ if (loopFocus) {
218
+ if (verticalDirection === 'up' && (prevIndex - verticalCols < minIndex || nextIndex < 0)) {
219
+ const col = prevIndex % verticalCols;
220
+ const maxCol = maxIndex % verticalCols;
221
+ const offset = maxIndex - (maxCol - col);
222
+
223
+ if (maxCol === col) {
224
+ nextIndex = maxIndex;
225
+ } else {
226
+ nextIndex = maxCol > col ? offset : offset - verticalCols;
227
+ }
228
+ if (onLoop) {
229
+ nextIndex = onLoop(event, prevIndex, nextIndex);
230
+ }
231
+ }
232
+
233
+ if (verticalDirection === 'down' && prevIndex + verticalCols > maxIndex) {
234
+ nextIndex = findNonDisabledListIndex(list, {
235
+ startingIndex: (prevIndex % verticalCols) - verticalCols,
236
+ amount: verticalCols,
237
+ disabledIndices,
238
+ });
239
+ if (onLoop) {
240
+ nextIndex = onLoop(event, prevIndex, nextIndex);
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ if (isIndexOutOfListBounds(list, nextIndex)) {
247
+ nextIndex = prevIndex;
248
+ }
249
+ }
250
+
251
+ // Remains on the same row/column.
252
+ if (orientation === 'both') {
253
+ const prevRow = floor(prevIndex / cols);
254
+
255
+ if (event.key === (rtl ? ARROW_LEFT : ARROW_RIGHT)) {
256
+ if (stop) {
257
+ stopEvent(event);
258
+ }
259
+
260
+ if (prevIndex % cols !== cols - 1) {
261
+ nextIndex = findNonDisabledListIndex(list, {
262
+ startingIndex: prevIndex,
263
+ disabledIndices,
264
+ });
265
+
266
+ if (loopFocus && isDifferentGridRow(nextIndex, cols, prevRow)) {
267
+ nextIndex = findNonDisabledListIndex(list, {
268
+ startingIndex: prevIndex - (prevIndex % cols) - 1,
269
+ disabledIndices,
270
+ });
271
+ if (onLoop) {
272
+ nextIndex = onLoop(event, prevIndex, nextIndex);
273
+ }
274
+ }
275
+ } else if (loopFocus) {
276
+ nextIndex = findNonDisabledListIndex(list, {
277
+ startingIndex: prevIndex - (prevIndex % cols) - 1,
278
+ disabledIndices,
279
+ });
280
+ if (onLoop) {
281
+ nextIndex = onLoop(event, prevIndex, nextIndex);
282
+ }
283
+ }
284
+
285
+ if (isDifferentGridRow(nextIndex, cols, prevRow)) {
286
+ nextIndex = prevIndex;
287
+ }
288
+ }
289
+
290
+ if (event.key === (rtl ? ARROW_RIGHT : ARROW_LEFT)) {
291
+ if (stop) {
292
+ stopEvent(event);
293
+ }
294
+
295
+ if (prevIndex % cols !== 0) {
296
+ nextIndex = findNonDisabledListIndex(list, {
297
+ startingIndex: prevIndex,
298
+ decrement: true,
299
+ disabledIndices,
300
+ });
301
+
302
+ if (loopFocus && isDifferentGridRow(nextIndex, cols, prevRow)) {
303
+ nextIndex = findNonDisabledListIndex(list, {
304
+ startingIndex: prevIndex + (cols - (prevIndex % cols)),
305
+ decrement: true,
306
+ disabledIndices,
307
+ });
308
+ if (onLoop) {
309
+ nextIndex = onLoop(event, prevIndex, nextIndex);
310
+ }
311
+ }
312
+ } else if (loopFocus) {
313
+ nextIndex = findNonDisabledListIndex(list, {
314
+ startingIndex: prevIndex + (cols - (prevIndex % cols)),
315
+ decrement: true,
316
+ disabledIndices,
317
+ });
318
+ if (onLoop) {
319
+ nextIndex = onLoop(event, prevIndex, nextIndex);
320
+ }
321
+ }
322
+
323
+ if (isDifferentGridRow(nextIndex, cols, prevRow)) {
324
+ nextIndex = prevIndex;
325
+ }
326
+ }
327
+
328
+ const lastRow = floor(maxIndex / cols) === prevRow;
329
+
330
+ if (isIndexOutOfListBounds(list, nextIndex)) {
331
+ if (loopFocus && lastRow) {
332
+ nextIndex =
333
+ event.key === (rtl ? ARROW_RIGHT : ARROW_LEFT)
334
+ ? maxIndex
335
+ : findNonDisabledListIndex(list, {
336
+ startingIndex: prevIndex - (prevIndex % cols) - 1,
337
+ disabledIndices,
338
+ });
339
+ if (onLoop) {
340
+ nextIndex = onLoop(event, prevIndex, nextIndex);
341
+ }
342
+ } else {
343
+ nextIndex = prevIndex;
344
+ }
345
+ }
346
+ }
347
+
348
+ return nextIndex;
349
+ }
350
+
351
+ /** For each cell index, gets the item index that occupies that cell */
352
+ export function createGridCellMap(
353
+ sizes: Dimensions[],
354
+ cols: number,
355
+ dense: boolean,
356
+ ): Array<number | undefined> {
357
+ const cellMap: (number | undefined)[] = [];
358
+ let startIndex = 0;
359
+ sizes.forEach(({ width, height }, index) => {
360
+ if (width > cols) {
361
+ // octane: dev-only upstream. Left unguarded because the alternative is the placement loop
362
+ // below spinning forever.
363
+ throw new Error(
364
+ `[Floating UI]: Invalid grid - item width at index ${index} is greater than grid columns`,
365
+ );
366
+ }
367
+ let itemPlaced = false;
368
+ if (dense) {
369
+ startIndex = 0;
370
+ }
371
+ while (!itemPlaced) {
372
+ const targetCells: number[] = [];
373
+ for (let i = 0; i < width; i += 1) {
374
+ for (let j = 0; j < height; j += 1) {
375
+ targetCells.push(startIndex + i + j * cols);
376
+ }
377
+ }
378
+ if (
379
+ (startIndex % cols) + width <= cols &&
380
+ targetCells.every((cell) => cellMap[cell] == null)
381
+ ) {
382
+ targetCells.forEach((cell) => {
383
+ cellMap[cell] = index;
384
+ });
385
+ itemPlaced = true;
386
+ } else {
387
+ startIndex += 1;
388
+ }
389
+ }
390
+ });
391
+
392
+ // convert into a non-sparse array
393
+ return [...cellMap];
394
+ }
395
+
396
+ /** Gets cell index of an item's corner or -1 when index is -1. */
397
+ export function getGridCellIndexOfCorner(
398
+ index: number,
399
+ sizes: Dimensions[],
400
+ cellMap: (number | undefined)[],
401
+ cols: number,
402
+ corner: 'tl' | 'tr' | 'bl' | 'br',
403
+ ): number {
404
+ if (index === -1) {
405
+ return -1;
406
+ }
407
+
408
+ const firstCellIndex = cellMap.indexOf(index);
409
+ const sizeItem = sizes[index];
410
+
411
+ switch (corner) {
412
+ case 'tl':
413
+ return firstCellIndex;
414
+ case 'tr':
415
+ if (!sizeItem) {
416
+ return firstCellIndex;
417
+ }
418
+ return firstCellIndex + sizeItem.width - 1;
419
+ case 'bl':
420
+ if (!sizeItem) {
421
+ return firstCellIndex;
422
+ }
423
+ return firstCellIndex + (sizeItem.height - 1) * cols;
424
+ case 'br':
425
+ return cellMap.lastIndexOf(index);
426
+ default:
427
+ return -1;
428
+ }
429
+ }
430
+
431
+ /** Gets all cell indices that correspond to the specified indices */
432
+ export function getGridCellIndices(
433
+ indices: (number | undefined)[],
434
+ cellMap: (number | undefined)[],
435
+ ): number[] {
436
+ return cellMap.flatMap((index, cellIndex) => (indices.includes(index) ? [cellIndex] : []));
437
+ }
@@ -0,0 +1,127 @@
1
+ // Ported from .base-ui/packages/react/src/internals/composite/root/gridNavigation.ts (v1.6.0).
2
+ // The navigator a composite passes to `useCompositeRoot`'s `grid` option — distinct from
3
+ // `../floating/gridNavigation`, which is the 1x1 navigator `useListNavigation` injects for itself.
4
+ //
5
+ // octane adaptations: the event is the NATIVE `KeyboardEvent`, and `React.RefObject<T>` is a plain
6
+ // `{ current: T }`. Not a hook, so no slot.
7
+ import {
8
+ createGridCellMap,
9
+ getGridCellIndexOfCorner,
10
+ getGridCellIndices,
11
+ getGridNavigatedIndex,
12
+ } from './grid-utils';
13
+ import { isListIndexDisabled } from './list-utils';
14
+ import { ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT } from './keys';
15
+
16
+ type CompositeGridElementsRef = { current: Array<HTMLElement | null> };
17
+ type CompositeGridOnLoop = (event: KeyboardEvent, prevIndex: number, nextIndex: number) => number;
18
+
19
+ export interface CompositeGridItemSize {
20
+ width: number;
21
+ height: number;
22
+ }
23
+
24
+ export interface CompositeGridConfig {
25
+ cols: number;
26
+ dense?: boolean | undefined;
27
+ itemSizes?: CompositeGridItemSize[] | undefined;
28
+ }
29
+
30
+ export interface CompositeGridNavigationState {
31
+ event: KeyboardEvent;
32
+ elementsRef: CompositeGridElementsRef;
33
+ highlightedIndex: number;
34
+ minIndex: number;
35
+ maxIndex: number;
36
+ orientation: 'horizontal' | 'vertical' | 'both';
37
+ loopFocus: boolean;
38
+ onLoop?: CompositeGridOnLoop | undefined;
39
+ disabledIndices?: number[] | undefined;
40
+ rtl: boolean;
41
+ }
42
+
43
+ export type CompositeGridNavigator = (state: CompositeGridNavigationState) => number;
44
+
45
+ /**
46
+ * Builds the grid navigation handler passed to `CompositeRoot`/`useCompositeRoot`
47
+ * via the `grid` prop. Importing and calling this is the opt-in for grid
48
+ * navigation: composites that don't pass `grid` never reference the algorithm,
49
+ * so bundlers tree-shake the grid helpers out.
50
+ */
51
+ export function gridNavigation(config: CompositeGridConfig): CompositeGridNavigator {
52
+ const { cols, dense = false, itemSizes } = config;
53
+
54
+ return (state) => {
55
+ const {
56
+ disabledIndices,
57
+ elementsRef,
58
+ event,
59
+ highlightedIndex,
60
+ loopFocus,
61
+ maxIndex,
62
+ minIndex,
63
+ onLoop,
64
+ orientation,
65
+ rtl,
66
+ } = state;
67
+
68
+ const sizes =
69
+ itemSizes ||
70
+ Array.from({ length: elementsRef.current.length }, () => ({
71
+ width: 1,
72
+ height: 1,
73
+ }));
74
+ // Work in hypothetical 1x1 cell indices, then convert back to item indices.
75
+ const cellMap = createGridCellMap(sizes, cols, dense);
76
+ const minGridIndex = cellMap.findIndex(
77
+ (index) => index != null && !isListIndexDisabled(elementsRef.current, index, disabledIndices),
78
+ );
79
+ const maxGridIndex = cellMap.reduce(
80
+ (foundIndex: number, index, cellIndex) =>
81
+ index != null && !isListIndexDisabled(elementsRef.current, index, disabledIndices)
82
+ ? cellIndex
83
+ : foundIndex,
84
+ -1,
85
+ );
86
+ const cellIndex = getGridNavigatedIndex(
87
+ cellMap.map((itemIndex) => (itemIndex != null ? elementsRef.current[itemIndex] : null)),
88
+ {
89
+ event,
90
+ orientation,
91
+ loopFocus,
92
+ onLoop,
93
+ cols,
94
+ // Treat undefined gaps as disabled so navigation cannot land in them.
95
+ disabledIndices: getGridCellIndices(
96
+ [
97
+ ...(disabledIndices ||
98
+ elementsRef.current.map((_, index) =>
99
+ isListIndexDisabled(elementsRef.current, index) ? index : undefined,
100
+ )),
101
+ undefined,
102
+ ],
103
+ cellMap,
104
+ ),
105
+ minIndex: minGridIndex,
106
+ maxIndex: maxGridIndex,
107
+ prevIndex: getGridCellIndexOfCorner(
108
+ highlightedIndex > maxIndex ? minIndex : highlightedIndex,
109
+ sizes,
110
+ cellMap,
111
+ cols,
112
+ // Choose the corner closest to the movement direction so spanning items
113
+ // do not immediately resolve back to themselves.
114
+ event.key === ARROW_DOWN
115
+ ? 'bl'
116
+ : event.key === (rtl ? ARROW_LEFT : ARROW_RIGHT)
117
+ ? 'tr'
118
+ : 'tl',
119
+ ),
120
+ rtl,
121
+ },
122
+ );
123
+
124
+ // Navigated cell will never be nullish.
125
+ return cellMap[cellIndex] as number;
126
+ };
127
+ }
@@ -1,7 +1,18 @@
1
1
  // Vendored list-navigation helpers — Base UI itself vendors these from `floating-ui-react`
2
- // (`.base-ui/packages/react/src/floating-ui-react/utils.ts`); octane's `@octanejs/floating-ui`
3
- // implements the identical functions but does not export them publicly, so we keep a small
4
- // self-contained copy here (matching the octane port's implementations verbatim).
2
+ // (`.base-ui/packages/react/src/floating-ui-react/utils/composite.ts`, re-exported through
3
+ // `internals/composite/composite.ts`); octane's `@octanejs/floating-ui` implements the identical
4
+ // functions but does not export them publicly, so we keep a small self-contained copy here.
5
+ //
6
+ // Signatures follow Base UI 1.6.0 exactly: the index helpers take the item ARRAY, while
7
+ // `getMinListIndex`/`getMaxListIndex` take the REF (they need `listRef.current.length` at call
8
+ // time). `isListIndexDisabled` is likewise the 1.6.0 body: an explicit `disabledIndices` hit wins,
9
+ // an invisible element is always disabled, and the `disabled`/`aria-disabled` attribute fallback
10
+ // applies only when the consumer passed no `disabledIndices` at all. `useListNavigation` depends on
11
+ // that last clause — it deliberately omits `disabledIndices` when picking the item to focus on open
12
+ // so attribute-disabled items are still skipped for consumers passing an empty array.
13
+ import { isElementVisible } from '../floating/composite';
14
+
15
+ export type DisabledIndices = ReadonlyArray<number> | ((index: number) => boolean);
5
16
 
6
17
  export function getTarget(event: any): EventTarget | null {
7
18
  if ('composedPath' in event) {
@@ -15,36 +26,53 @@ export function stopEvent(event: any): void {
15
26
  event.stopPropagation();
16
27
  }
17
28
 
18
- export function isIndexOutOfListBounds(listRef: { current: any[] }, index: number): boolean {
19
- return index < 0 || index >= listRef.current.length;
29
+ export function isDifferentGridRow(index: number, cols: number, prevRow: number): boolean {
30
+ return Math.floor(index / cols) !== prevRow;
31
+ }
32
+
33
+ export function isIndexOutOfListBounds(
34
+ list: ReadonlyArray<HTMLElement | null>,
35
+ index: number,
36
+ ): boolean {
37
+ return index < 0 || index >= list.length;
20
38
  }
21
39
 
22
40
  export function isListIndexDisabled(
23
- listRef: { current: any[] },
41
+ list: ReadonlyArray<HTMLElement | null>,
24
42
  index: number,
25
- disabledIndices?: number[] | ((index: number) => boolean),
43
+ disabledIndices?: DisabledIndices | undefined,
26
44
  ): boolean {
27
- if (typeof disabledIndices === 'function') {
28
- return disabledIndices(index);
45
+ const isExplicitlyDisabled =
46
+ typeof disabledIndices === 'function'
47
+ ? disabledIndices(index)
48
+ : (disabledIndices?.includes(index) ?? false);
49
+
50
+ if (isExplicitlyDisabled) {
51
+ return true;
29
52
  }
30
- if (disabledIndices) {
31
- return disabledIndices.includes(index);
53
+
54
+ const element = list[index];
55
+ if (!element) {
56
+ return false;
32
57
  }
33
- const element = listRef.current[index];
58
+
59
+ if (!isElementVisible(element)) {
60
+ return true;
61
+ }
62
+
34
63
  return (
35
- element == null ||
36
- element.hasAttribute('disabled') ||
37
- element.getAttribute('aria-disabled') === 'true'
64
+ !disabledIndices &&
65
+ (element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true')
38
66
  );
39
67
  }
40
68
 
41
69
  export function findNonDisabledListIndex(
42
- listRef: { current: any[] },
70
+ list: ReadonlyArray<HTMLElement | null>,
43
71
  options: {
44
- startingIndex?: number;
45
- decrement?: boolean;
46
- disabledIndices?: number[] | ((index: number) => boolean);
47
- amount?: number;
72
+ startingIndex?: number | undefined;
73
+ decrement?: boolean | undefined;
74
+ disabledIndices?: DisabledIndices | undefined;
75
+ amount?: number | undefined;
48
76
  } = {},
49
77
  ): number {
50
78
  const { startingIndex = -1, decrement = false, disabledIndices, amount = 1 } = options;
@@ -53,24 +81,24 @@ export function findNonDisabledListIndex(
53
81
  index += decrement ? -amount : amount;
54
82
  } while (
55
83
  index >= 0 &&
56
- index <= listRef.current.length - 1 &&
57
- isListIndexDisabled(listRef, index, disabledIndices)
84
+ index <= list.length - 1 &&
85
+ isListIndexDisabled(list, index, disabledIndices)
58
86
  );
59
87
  return index;
60
88
  }
61
89
 
62
90
  export function getMinListIndex(
63
- listRef: { current: any[] },
64
- disabledIndices: number[] | ((index: number) => boolean) | undefined,
91
+ listRef: { current: ReadonlyArray<HTMLElement | null> },
92
+ disabledIndices?: DisabledIndices | undefined,
65
93
  ): number {
66
- return findNonDisabledListIndex(listRef, { disabledIndices });
94
+ return findNonDisabledListIndex(listRef.current, { disabledIndices });
67
95
  }
68
96
 
69
97
  export function getMaxListIndex(
70
- listRef: { current: any[] },
71
- disabledIndices: number[] | ((index: number) => boolean) | undefined,
98
+ listRef: { current: ReadonlyArray<HTMLElement | null> },
99
+ disabledIndices?: DisabledIndices | undefined,
72
100
  ): number {
73
- return findNonDisabledListIndex(listRef, {
101
+ return findNonDisabledListIndex(listRef.current, {
74
102
  decrement: true,
75
103
  startingIndex: listRef.current.length,
76
104
  disabledIndices,