@mui/utils 9.0.0-alpha.1 → 9.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # [Versions](https://mui.com/versions/)
2
2
 
3
+ ## 9.0.0-alpha.3
4
+
5
+ <!-- generated comparing v9.0.0-alpha.2..master -->
6
+
7
+ _Mar 12, 2026_
8
+
9
+ A big thanks to the 10 contributors who made this release possible. Here are some highlights ✨:
10
+
11
+ - 📖 A new [Menubar](https://mui.com/material-ui/react-menubar/) component page integrated with [Base UI](https://base-ui.com/react/components/menubar)
12
+ - ♿️ Improved the Roving TabIndex keyboard navigation for the Stepper, Tabs and MenuList components.
13
+
14
+ ### `@mui/material@9.0.0-alpha.3`
15
+
16
+ - [autocomplete] Add `root` slot (#47852) @GerardasB
17
+ - [autocomplete] Fix popup reopening on window focus regain with openOnFocus (#47790) @aman44444
18
+ - [autocomplete] Support full slots for clearIndicator and popupIndicator (#47891) @silviuaavram
19
+ - [material-ui] Partially revert "[material-ui] Clean up duplicated CSS rules (#47838)" (#47927) @sai6855
20
+ - [stepper][menulist][tabs] Improve accessibility (#47687) @silviuaavram
21
+
22
+ ### Docs
23
+
24
+ - [docs][codemod] Add v7 migration docs for deprecated Autocomplete APIs and Autocomplete codemod (#47945) @ZeeshanTamboli
25
+ - [docs] Update faq about vendor chunks (#47747) @Janpot
26
+ - [docs] Use direct palette vars in Tailwind v4 snippet (#47940) @Ahmad-Alaziz
27
+ - [docs][menubar] Add Menubar component page (#47616) @siriwatknp
28
+
29
+ ### Core
30
+
31
+ - [core] Fix the release prepare steps (#47951) @silviuaavram
32
+ - [core] Remove Joy UI code and docs (#47939) @mnajdova
33
+ - [code-infra] Add previously missed export of themeCssVarsAugmentation (#47918) @brijeshb42
34
+ - [docs-infra] Import font module for nextjs transpilation (#47935) @brijeshb42
35
+ - [docs-infra] Migrate simpler modules from docs to mui-docs (#47897) @brijeshb42
36
+ - [test] Fix detached anchorEl elements in tests (#47929) @Janpot
37
+
38
+ All contributors of this release in alphabetical order: @Ahmad-Alaziz, @aman44444, @brijeshb42, @GerardasB, @Janpot, @mnajdova, @sai6855, @silviuaavram, @siriwatknp, @ZeeshanTamboli
39
+
40
+ ## 9.0.0-alpha.2
41
+
42
+ <!-- generated comparing v9.0.0-alpha.1..master -->
43
+
44
+ _Mar 5, 2026_
45
+
46
+ A big thanks to the 4 contributors who made this release possible.
47
+
48
+ ### @mui/material@9.0.0-alpha.2
49
+
50
+ - Clean up duplicated CSS rules (#47838) @sai6855
51
+
52
+ ### @mui/system@9.0.0-alpha.2
53
+
54
+ - Refactor sortContainerQueries to define regex outside of sort function (#47817) @sai6855
55
+
56
+ ### Docs
57
+
58
+ - Move shared components to @mui/docs package (#47672) @Janpot
59
+ - Fix small typo in NumberField page (#47877) @arthur-plazanet
60
+
61
+ ### Core
62
+
63
+ - [code-infra] Reduce paths for attw checks (#47896) @brijeshb42
64
+ - [docs-infra] Run syncTeamMembers (#47900) @Janpot
65
+
66
+ All contributors of this release in alphabetical order: @arthur-plazanet, @brijeshb42, @Janpot, @sai6855
67
+
3
68
  ## 9.0.0-alpha.1
4
69
 
5
70
  <!-- generated comparing v9.0.0-alpha.0..master -->
package/index.d.mts CHANGED
@@ -49,4 +49,5 @@ export { default as unstable_resolveComponentProps } from "./resolveComponentPro
49
49
  export { default as unstable_extractEventHandlers } from "./extractEventHandlers/index.mjs";
50
50
  export { default as unstable_getReactNodeRef } from "./getReactNodeRef/index.mjs";
51
51
  export { default as unstable_getReactElementRef } from "./getReactElementRef/index.mjs";
52
+ export { default as unstable_useRovingTabIndex } from "./useRovingTabIndex/index.mjs";
52
53
  export * from "./types/index.mjs";
package/index.d.ts CHANGED
@@ -49,4 +49,5 @@ export { default as unstable_resolveComponentProps } from "./resolveComponentPro
49
49
  export { default as unstable_extractEventHandlers } from "./extractEventHandlers/index.js";
50
50
  export { default as unstable_getReactNodeRef } from "./getReactNodeRef/index.js";
51
51
  export { default as unstable_getReactElementRef } from "./getReactElementRef/index.js";
52
+ export { default as unstable_useRovingTabIndex } from "./useRovingTabIndex/index.js";
52
53
  export * from "./types/index.js";
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/utils v9.0.0-alpha.1
2
+ * @mui/utils v9.0.0-alpha.3
3
3
  *
4
4
  * @license MIT
5
5
  * This source code is licensed under the MIT license found in the
@@ -62,7 +62,8 @@ var _exportNames = {
62
62
  unstable_resolveComponentProps: true,
63
63
  unstable_extractEventHandlers: true,
64
64
  unstable_getReactNodeRef: true,
65
- unstable_getReactElementRef: true
65
+ unstable_getReactElementRef: true,
66
+ unstable_useRovingTabIndex: true
66
67
  };
67
68
  Object.defineProperty(exports, "HTMLElementType", {
68
69
  enumerable: true,
@@ -340,6 +341,12 @@ Object.defineProperty(exports, "unstable_useOnMount", {
340
341
  return _useOnMount.default;
341
342
  }
342
343
  });
344
+ Object.defineProperty(exports, "unstable_useRovingTabIndex", {
345
+ enumerable: true,
346
+ get: function () {
347
+ return _useRovingTabIndex.default;
348
+ }
349
+ });
343
350
  Object.defineProperty(exports, "unstable_useSlotProps", {
344
351
  enumerable: true,
345
352
  get: function () {
@@ -422,6 +429,7 @@ var _resolveComponentProps = _interopRequireDefault(require("./resolveComponentP
422
429
  var _extractEventHandlers = _interopRequireDefault(require("./extractEventHandlers"));
423
430
  var _getReactNodeRef = _interopRequireDefault(require("./getReactNodeRef"));
424
431
  var _getReactElementRef = _interopRequireDefault(require("./getReactElementRef"));
432
+ var _useRovingTabIndex = _interopRequireDefault(require("./useRovingTabIndex"));
425
433
  var _types = require("./types");
426
434
  Object.keys(_types).forEach(function (key) {
427
435
  if (key === "default" || key === "__esModule") return;
package/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/utils v9.0.0-alpha.1
2
+ * @mui/utils v9.0.0-alpha.3
3
3
  *
4
4
  * @license MIT
5
5
  * This source code is licensed under the MIT license found in the
@@ -55,4 +55,5 @@ export { default as unstable_resolveComponentProps } from "./resolveComponentPro
55
55
  export { default as unstable_extractEventHandlers } from "./extractEventHandlers/index.mjs";
56
56
  export { default as unstable_getReactNodeRef } from "./getReactNodeRef/index.mjs";
57
57
  export { default as unstable_getReactElementRef } from "./getReactElementRef/index.mjs";
58
+ export { default as unstable_useRovingTabIndex } from "./useRovingTabIndex/index.mjs";
58
59
  export * from "./types/index.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/utils",
3
- "version": "9.0.0-alpha.1",
3
+ "version": "9.0.0-alpha.3",
4
4
  "author": "MUI Team",
5
5
  "description": "Utility functions for React components.",
6
6
  "keywords": [
@@ -29,7 +29,7 @@
29
29
  "clsx": "^2.1.1",
30
30
  "prop-types": "^15.8.1",
31
31
  "react-is": "^19.2.4",
32
- "@mui/types": "^9.0.0-alpha.1"
32
+ "@mui/types": "^9.0.0-alpha.3"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
@@ -764,6 +764,20 @@
764
764
  "default": "./usePreviousProps/index.mjs"
765
765
  }
766
766
  },
767
+ "./useRovingTabIndex": {
768
+ "import": {
769
+ "types": "./useRovingTabIndex/index.d.mts",
770
+ "default": "./useRovingTabIndex/index.mjs"
771
+ },
772
+ "require": {
773
+ "types": "./useRovingTabIndex/index.d.ts",
774
+ "default": "./useRovingTabIndex/index.js"
775
+ },
776
+ "default": {
777
+ "types": "./useRovingTabIndex/index.d.mts",
778
+ "default": "./useRovingTabIndex/index.mjs"
779
+ }
780
+ },
767
781
  "./useSlotProps": {
768
782
  "import": {
769
783
  "types": "./useSlotProps/index.d.mts",
@@ -0,0 +1 @@
1
+ export { default } from "./useRovingTabIndex.mjs";
@@ -0,0 +1 @@
1
+ export { default } from "./useRovingTabIndex.js";
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ Object.defineProperty(exports, "default", {
8
+ enumerable: true,
9
+ get: function () {
10
+ return _useRovingTabIndex.default;
11
+ }
12
+ });
13
+ var _useRovingTabIndex = _interopRequireDefault(require("./useRovingTabIndex"));
@@ -0,0 +1 @@
1
+ export { default } from "./useRovingTabIndex.mjs";
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+ export type UseRovingTabIndexOptions = {
3
+ focusableIndex?: number | undefined;
4
+ orientation: 'horizontal' | 'vertical';
5
+ isRtl?: boolean | undefined;
6
+ shouldFocus?: ((element: HTMLElement | null) => boolean) | undefined;
7
+ shouldWrap?: boolean | undefined;
8
+ };
9
+ type UseRovingTabIndexReturn = {
10
+ getItemProps: (index: number, ref?: React.Ref<HTMLElement>) => {
11
+ ref: (element: HTMLElement | null) => void;
12
+ tabIndex: number;
13
+ };
14
+ getContainerProps: (ref?: React.Ref<HTMLElement>) => {
15
+ onFocus: (event: React.FocusEvent<HTMLElement>) => void;
16
+ onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
17
+ ref: (element: HTMLElement | null) => void;
18
+ };
19
+ focusNext: (shouldSkipFocusOverride?: (element: HTMLElement | null) => boolean) => number;
20
+ };
21
+ /**
22
+ * Provides roving tab index behavior for a container and its focusable children.
23
+ * This is useful for implementing keyboard navigation in components like menus, tabs, and lists.
24
+ * The hook manages the focus state of child elements and provides props to be spread on both the container and the items.
25
+ * The container will handle keyboard events to move focus between items based on the specified orientation and wrapping behavior.
26
+ *
27
+ * @param options - Configuration options for the roving tab index behavior, including orientation, initial focusable index, RTL support, and custom focus logic.
28
+ * @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.
29
+ */
30
+ export default function useRovingTabIndex(options: UseRovingTabIndexOptions): UseRovingTabIndexReturn;
31
+ export {};
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+ export type UseRovingTabIndexOptions = {
3
+ focusableIndex?: number | undefined;
4
+ orientation: 'horizontal' | 'vertical';
5
+ isRtl?: boolean | undefined;
6
+ shouldFocus?: ((element: HTMLElement | null) => boolean) | undefined;
7
+ shouldWrap?: boolean | undefined;
8
+ };
9
+ type UseRovingTabIndexReturn = {
10
+ getItemProps: (index: number, ref?: React.Ref<HTMLElement>) => {
11
+ ref: (element: HTMLElement | null) => void;
12
+ tabIndex: number;
13
+ };
14
+ getContainerProps: (ref?: React.Ref<HTMLElement>) => {
15
+ onFocus: (event: React.FocusEvent<HTMLElement>) => void;
16
+ onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
17
+ ref: (element: HTMLElement | null) => void;
18
+ };
19
+ focusNext: (shouldSkipFocusOverride?: (element: HTMLElement | null) => boolean) => number;
20
+ };
21
+ /**
22
+ * Provides roving tab index behavior for a container and its focusable children.
23
+ * This is useful for implementing keyboard navigation in components like menus, tabs, and lists.
24
+ * The hook manages the focus state of child elements and provides props to be spread on both the container and the items.
25
+ * The container will handle keyboard events to move focus between items based on the specified orientation and wrapping behavior.
26
+ *
27
+ * @param options - Configuration options for the roving tab index behavior, including orientation, initial focusable index, RTL support, and custom focus logic.
28
+ * @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.
29
+ */
30
+ export default function useRovingTabIndex(options: UseRovingTabIndexOptions): UseRovingTabIndexReturn;
31
+ export {};
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ 'use client';
3
+
4
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
5
+ var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
6
+ Object.defineProperty(exports, "__esModule", {
7
+ value: true
8
+ });
9
+ exports.default = useRovingTabIndex;
10
+ var React = _interopRequireWildcard(require("react"));
11
+ var _ownerDocument = _interopRequireDefault(require("../ownerDocument"));
12
+ var _getActiveElement = _interopRequireDefault(require("../getActiveElement"));
13
+ const SUPPORTED_KEYS = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
14
+
15
+ /**
16
+ * Provides roving tab index behavior for a container and its focusable children.
17
+ * This is useful for implementing keyboard navigation in components like menus, tabs, and lists.
18
+ * The hook manages the focus state of child elements and provides props to be spread on both the container and the items.
19
+ * 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
+ */
24
+ function useRovingTabIndex(options) {
25
+ const {
26
+ orientation,
27
+ focusableIndex: focusableIndexProp,
28
+ 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);
41
+ }
42
+ }
43
+ React.useEffect(() => {
44
+ if (elementsRef.current.length === 0 || focusableIndex === -1 || focusableIndex >= elementsRef.current.length) {
45
+ return;
46
+ }
47
+ if (!shouldFocus(elementsRef.current[focusableIndex])) {
48
+ const nextIndex = focusNext(elementsRef, focusableIndex, 'next', false, shouldFocus);
49
+ setFocusableIndex(nextIndex);
50
+ }
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]);
58
+ const getContainerProps = React.useCallback(ref => {
59
+ const onFocus = event => {
60
+ const focusedElement = event.target;
61
+ const focusedIndex = elementsRef.current.findIndex(element => element === focusedElement);
62
+ if (focusedIndex !== -1) {
63
+ setFocusableIndex(focusedIndex);
64
+ }
65
+ };
66
+ const onKeyDown = event => {
67
+ if (event.altKey || event.shiftKey || event.ctrlKey || event.metaKey) {
68
+ return;
69
+ }
70
+ if (!SUPPORTED_KEYS.includes(event.key)) {
71
+ return;
72
+ }
73
+ let previousItemKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
74
+ let nextItemKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
75
+ if (orientation === 'horizontal' && isRtl) {
76
+ // swap previousItemKey with nextItemKey
77
+ previousItemKey = 'ArrowRight';
78
+ nextItemKey = 'ArrowLeft';
79
+ }
80
+ const currentFocus = (0, _getActiveElement.default)((0, _ownerDocument.default)(containerRef.current));
81
+ const isFocusOnContainer = currentFocus === containerRef.current;
82
+ let direction = 'next';
83
+ let currentIndex = focusableIndex;
84
+ switch (event.key) {
85
+ case previousItemKey:
86
+ direction = 'previous';
87
+ event.preventDefault();
88
+ if (isFocusOnContainer) {
89
+ // Set to length, so that the previous focused element will be the last one.
90
+ currentIndex = elementsRef.current.length;
91
+ }
92
+ break;
93
+ case nextItemKey:
94
+ event.preventDefault();
95
+ if (isFocusOnContainer) {
96
+ // Set to -1, so that the next focused element will be the first one.
97
+ currentIndex = -1;
98
+ }
99
+ break;
100
+ case 'Home':
101
+ event.preventDefault();
102
+ // Set to -1, so that the next focused element will be the first one.
103
+ currentIndex = -1;
104
+ break;
105
+ case 'End':
106
+ event.preventDefault();
107
+ direction = 'previous';
108
+ // Set to length, so that the previous focused element will be the last one.
109
+ currentIndex = elementsRef.current.length;
110
+ break;
111
+ default:
112
+ return;
113
+ }
114
+ focusNext(elementsRef, currentIndex, direction, shouldWrap, shouldFocus);
115
+ };
116
+ return {
117
+ onFocus,
118
+ onKeyDown,
119
+ ref: handleRefs(ref, elementNode => {
120
+ containerRef.current = elementNode;
121
+ })
122
+ };
123
+ }, [focusableIndex, isRtl, orientation, shouldWrap, shouldFocus]);
124
+ const focusNextExport = React.useCallback(shouldFocusOverride => {
125
+ const currentFocus = (0, _getActiveElement.default)((0, _ownerDocument.default)(containerRef.current));
126
+ const isFocusOnContainer = currentFocus === containerRef.current;
127
+ let currentIndex = focusableIndex;
128
+ if (isFocusOnContainer) {
129
+ currentIndex = -1;
130
+ }
131
+ const nextIndex = focusNext(elementsRef, currentIndex, 'next', true, shouldFocusOverride ?? shouldFocus);
132
+ if (nextIndex !== -1) {
133
+ setFocusableIndex(nextIndex);
134
+ }
135
+ return nextIndex;
136
+ }, [focusableIndex, shouldFocus]);
137
+ return {
138
+ getItemProps,
139
+ getContainerProps,
140
+ focusNext: focusNextExport
141
+ };
142
+ }
143
+ function focusNext(elementsRef, currentIndex, direction, wrap, shouldFocus) {
144
+ const lastIndex = elementsRef.current.length - 1;
145
+ let wrappedOnce = false;
146
+ let nextIndex = getNextIndex(currentIndex, lastIndex, direction, wrap);
147
+ const startIndex = nextIndex;
148
+ while (nextIndex !== -1) {
149
+ // Prevent infinite loop.
150
+ if (nextIndex === startIndex) {
151
+ if (wrappedOnce) {
152
+ return -1;
153
+ }
154
+ wrappedOnce = true;
155
+ }
156
+ const nextElement = elementsRef.current[nextIndex];
157
+
158
+ // Same logic as useAutocomplete.js
159
+ if (!shouldFocus(nextElement)) {
160
+ // Move to the next element.
161
+ nextIndex = getNextIndex(nextIndex, lastIndex, direction, wrap);
162
+ } else {
163
+ nextElement?.focus();
164
+ return nextIndex;
165
+ }
166
+ }
167
+ return -1;
168
+ }
169
+ function getNextIndex(currentIndex, lastIndex, direction, wrap = true) {
170
+ if (direction === 'next') {
171
+ if (currentIndex === lastIndex) {
172
+ return wrap ? 0 : -1;
173
+ }
174
+ return currentIndex + 1;
175
+ }
176
+ if (currentIndex === 0) {
177
+ return wrap ? lastIndex : -1;
178
+ }
179
+ return currentIndex - 1;
180
+ }
181
+ function internalShouldFocus(element) {
182
+ if (!element) {
183
+ return false;
184
+ }
185
+ return !element.hasAttribute('disabled') && element.getAttribute('aria-disabled') !== 'true' && element.hasAttribute('tabindex');
186
+ }
187
+ function handleRefs(...refs) {
188
+ return node => {
189
+ refs.forEach(ref => {
190
+ if (typeof ref === 'function') {
191
+ ref(node);
192
+ } else if (ref) {
193
+ ref.current = node;
194
+ }
195
+ });
196
+ };
197
+ }
@@ -0,0 +1,190 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import ownerDocument from "../ownerDocument/index.mjs";
5
+ import getActiveElement from "../getActiveElement/index.mjs";
6
+ const SUPPORTED_KEYS = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
7
+
8
+ /**
9
+ * Provides roving tab index behavior for a container and its focusable children.
10
+ * This is useful for implementing keyboard navigation in components like menus, tabs, and lists.
11
+ * The hook manages the focus state of child elements and provides props to be spread on both the container and the items.
12
+ * The container will handle keyboard events to move focus between items based on the specified orientation and wrapping behavior.
13
+ *
14
+ * @param options - Configuration options for the roving tab index behavior, including orientation, initial focusable index, RTL support, and custom focus logic.
15
+ * @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.
16
+ */
17
+ export default function useRovingTabIndex(options) {
18
+ const {
19
+ orientation,
20
+ focusableIndex: focusableIndexProp,
21
+ isRtl = false,
22
+ shouldFocus = internalShouldFocus,
23
+ shouldWrap = true
24
+ } = options;
25
+ const initialFocusableIndex = focusableIndexProp ?? 0;
26
+ const [focusableIndex, setFocusableIndex] = React.useState(initialFocusableIndex);
27
+ const elementsRef = React.useRef([]);
28
+ const containerRef = React.useRef(null);
29
+ const previousFocusableIndexPropRef = React.useRef(initialFocusableIndex);
30
+ if (focusableIndexProp !== undefined && focusableIndexProp !== previousFocusableIndexPropRef.current) {
31
+ previousFocusableIndexPropRef.current = focusableIndexProp;
32
+ if (focusableIndexProp !== focusableIndex) {
33
+ setFocusableIndex(focusableIndexProp);
34
+ }
35
+ }
36
+ React.useEffect(() => {
37
+ if (elementsRef.current.length === 0 || focusableIndex === -1 || focusableIndex >= elementsRef.current.length) {
38
+ return;
39
+ }
40
+ if (!shouldFocus(elementsRef.current[focusableIndex])) {
41
+ const nextIndex = focusNext(elementsRef, focusableIndex, 'next', false, shouldFocus);
42
+ setFocusableIndex(nextIndex);
43
+ }
44
+ }, [focusableIndex, shouldFocus]);
45
+ const getItemProps = React.useCallback((index, ref) => ({
46
+ ref: handleRefs(ref, elementNode => {
47
+ elementsRef.current[index] = elementNode;
48
+ }),
49
+ tabIndex: index === focusableIndex ? 0 : -1
50
+ }), [focusableIndex]);
51
+ const getContainerProps = React.useCallback(ref => {
52
+ const onFocus = event => {
53
+ const focusedElement = event.target;
54
+ const focusedIndex = elementsRef.current.findIndex(element => element === focusedElement);
55
+ if (focusedIndex !== -1) {
56
+ setFocusableIndex(focusedIndex);
57
+ }
58
+ };
59
+ const onKeyDown = event => {
60
+ if (event.altKey || event.shiftKey || event.ctrlKey || event.metaKey) {
61
+ return;
62
+ }
63
+ if (!SUPPORTED_KEYS.includes(event.key)) {
64
+ return;
65
+ }
66
+ let previousItemKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
67
+ let nextItemKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
68
+ if (orientation === 'horizontal' && isRtl) {
69
+ // swap previousItemKey with nextItemKey
70
+ previousItemKey = 'ArrowRight';
71
+ nextItemKey = 'ArrowLeft';
72
+ }
73
+ const currentFocus = getActiveElement(ownerDocument(containerRef.current));
74
+ const isFocusOnContainer = currentFocus === containerRef.current;
75
+ let direction = 'next';
76
+ let currentIndex = focusableIndex;
77
+ switch (event.key) {
78
+ case previousItemKey:
79
+ direction = 'previous';
80
+ event.preventDefault();
81
+ if (isFocusOnContainer) {
82
+ // Set to length, so that the previous focused element will be the last one.
83
+ currentIndex = elementsRef.current.length;
84
+ }
85
+ break;
86
+ case nextItemKey:
87
+ event.preventDefault();
88
+ if (isFocusOnContainer) {
89
+ // Set to -1, so that the next focused element will be the first one.
90
+ currentIndex = -1;
91
+ }
92
+ break;
93
+ case 'Home':
94
+ event.preventDefault();
95
+ // Set to -1, so that the next focused element will be the first one.
96
+ currentIndex = -1;
97
+ break;
98
+ case 'End':
99
+ event.preventDefault();
100
+ direction = 'previous';
101
+ // Set to length, so that the previous focused element will be the last one.
102
+ currentIndex = elementsRef.current.length;
103
+ break;
104
+ default:
105
+ return;
106
+ }
107
+ focusNext(elementsRef, currentIndex, direction, shouldWrap, shouldFocus);
108
+ };
109
+ return {
110
+ onFocus,
111
+ onKeyDown,
112
+ ref: handleRefs(ref, elementNode => {
113
+ containerRef.current = elementNode;
114
+ })
115
+ };
116
+ }, [focusableIndex, isRtl, orientation, shouldWrap, shouldFocus]);
117
+ const focusNextExport = React.useCallback(shouldFocusOverride => {
118
+ const currentFocus = getActiveElement(ownerDocument(containerRef.current));
119
+ const isFocusOnContainer = currentFocus === containerRef.current;
120
+ let currentIndex = focusableIndex;
121
+ if (isFocusOnContainer) {
122
+ currentIndex = -1;
123
+ }
124
+ const nextIndex = focusNext(elementsRef, currentIndex, 'next', true, shouldFocusOverride ?? shouldFocus);
125
+ if (nextIndex !== -1) {
126
+ setFocusableIndex(nextIndex);
127
+ }
128
+ return nextIndex;
129
+ }, [focusableIndex, shouldFocus]);
130
+ return {
131
+ getItemProps,
132
+ getContainerProps,
133
+ focusNext: focusNextExport
134
+ };
135
+ }
136
+ function focusNext(elementsRef, currentIndex, direction, wrap, shouldFocus) {
137
+ const lastIndex = elementsRef.current.length - 1;
138
+ let wrappedOnce = false;
139
+ let nextIndex = getNextIndex(currentIndex, lastIndex, direction, wrap);
140
+ const startIndex = nextIndex;
141
+ while (nextIndex !== -1) {
142
+ // Prevent infinite loop.
143
+ if (nextIndex === startIndex) {
144
+ if (wrappedOnce) {
145
+ return -1;
146
+ }
147
+ wrappedOnce = true;
148
+ }
149
+ const nextElement = elementsRef.current[nextIndex];
150
+
151
+ // Same logic as useAutocomplete.js
152
+ if (!shouldFocus(nextElement)) {
153
+ // Move to the next element.
154
+ nextIndex = getNextIndex(nextIndex, lastIndex, direction, wrap);
155
+ } else {
156
+ nextElement?.focus();
157
+ return nextIndex;
158
+ }
159
+ }
160
+ return -1;
161
+ }
162
+ function getNextIndex(currentIndex, lastIndex, direction, wrap = true) {
163
+ if (direction === 'next') {
164
+ if (currentIndex === lastIndex) {
165
+ return wrap ? 0 : -1;
166
+ }
167
+ return currentIndex + 1;
168
+ }
169
+ if (currentIndex === 0) {
170
+ return wrap ? lastIndex : -1;
171
+ }
172
+ return currentIndex - 1;
173
+ }
174
+ function internalShouldFocus(element) {
175
+ if (!element) {
176
+ return false;
177
+ }
178
+ return !element.hasAttribute('disabled') && element.getAttribute('aria-disabled') !== 'true' && element.hasAttribute('tabindex');
179
+ }
180
+ function handleRefs(...refs) {
181
+ return node => {
182
+ refs.forEach(ref => {
183
+ if (typeof ref === 'function') {
184
+ ref(node);
185
+ } else if (ref) {
186
+ ref.current = node;
187
+ }
188
+ });
189
+ };
190
+ }