@mui/utils 9.0.0-alpha.2 → 9.0.0-alpha.4

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,196 @@
1
1
  # [Versions](https://mui.com/versions/)
2
2
 
3
+ ## 9.0.0-alpha.4
4
+
5
+ <!-- generated comparing v9.0.0-alpha.3..master -->
6
+
7
+ _Mar 20, 2026_
8
+
9
+ A big thanks to the 11 contributors who made this release possible.
10
+
11
+ ### `@mui/material@9.0.0-alpha.4`
12
+
13
+ #### Breaking Changes
14
+
15
+ - [accordion] Remove deprecated props (#47963) @silviuaavram
16
+ - `TransitionComponent`
17
+ - `TransitionProps`
18
+ - [accordionsummary] Remove deprecated CSS class (#48006) @silviuaavram
19
+ - `contentGutters`
20
+ - [alert] Remove deprecated CSS classes (#48011) @silviuaavram
21
+ - `standardSuccess`
22
+ - `standardInfo`
23
+ - `standardWarning`
24
+ - `standardError`
25
+ - `outlinedSuccess`
26
+ - `outlinedInfo`
27
+ - `outlinedWarning`
28
+ - `outlinedError`
29
+ - `filledSuccess`
30
+ - `filledInfo`
31
+ - `filledWarning`
32
+ - `filledError`
33
+ - [alert] Remove deprecated props (#47965) @silviuaavram
34
+ - `components`
35
+ - `componentsProps`
36
+ - [autocomplete] Remove deprecated props (#47864) @ZeeshanTamboli
37
+ - `ChipProps`
38
+ - `componentsProps`
39
+ - `ListboxComponent`
40
+ - `ListboxProps`
41
+ - `PaperComponent`
42
+ - `PopperComponent`
43
+ - `renderTags`
44
+ - [avatar] Remove deprecated props (#47966) @silviuaavram
45
+ - `imgProps`
46
+ - [avatargroup] Remove deprecated props (#47968) @silviuaavram
47
+ - `componentsProps`
48
+ - [backdrop] Remove deprecated props (#47991) @silviuaavram
49
+ - `components`
50
+ - `componentsProps`
51
+ - `TransitionComponent`
52
+ - [badge] Remove deprecated props (#47993) @silviuaavram
53
+ - `components`
54
+ - `componentsProps`
55
+ - [button] Remove deprecated CSS classes (#48012) @silviuaavram
56
+ - Color variant classes: `textInherit`, `outlinedPrimary`, `containedSecondary`, etc.
57
+ - Size classes: `textSizeSmall`, `iconSizeMedium`, etc.
58
+ - [circularprogress] Remove deprecated CSS classes (#48014) @silviuaavram
59
+ - `circleDeterminate`
60
+ - `circleIndeterminate`
61
+ - [divider] Remove deprecated prop (#48013) @atharva3333
62
+ - `light`
63
+ - [formcontrollabel] Remove deprecated props (#48032) @atharva3333
64
+ - `componentsProps`
65
+ - [menu] Remove deprecated props (#48021) @siriwatknp
66
+ - `MenuListProps`
67
+ - `PaperProps`
68
+ - `TransitionProps`
69
+ - [mobilestepper] Remove deprecated props (#48041) @siriwatknp
70
+ - `LinearProgressProps`
71
+ - [popover] Remove deprecated props (#48021) @siriwatknp
72
+ - `BackdropComponent`
73
+ - `BackdropProps`
74
+ - `PaperProps`
75
+ - `TransitionComponent`
76
+ - `TransitionProps`
77
+ - [popper] Remove deprecated props (#48020) @siriwatknp
78
+ - `components`
79
+ - `componentsProps`
80
+ - [select] Remove deprecated props passed via `MenuProps` (#48021) @siriwatknp
81
+ - `MenuListProps`
82
+ - `PaperProps`
83
+ - `TransitionProps`
84
+ - [slider] Remove deprecated props (#47996) @siriwatknp
85
+ - `components`
86
+ - `componentsProps`
87
+ - [speeddial] Remove deprecated props (#47998) @siriwatknp
88
+ - `TransitionComponent`
89
+ - `TransitionProps`
90
+ - [speeddialaction] Remove deprecated props (#47998) @siriwatknp
91
+ - `FabProps`
92
+ - `tooltipTitle`
93
+ - `tooltipPlacement`
94
+ - `tooltipOpen`
95
+ - `TooltipClasses`
96
+ - [tabs] Remove deprecated props (#48017) @siriwatknp
97
+ - `ScrollButtonComponent`
98
+ - `TabIndicatorProps`
99
+ - `TabScrollButtonProps`
100
+ - `slots.StartScrollButtonIcon`
101
+ - `slots.EndScrollButtonIcon`
102
+ - [textfield] Remove deprecated props (#47878) @ZeeshanTamboli
103
+ - `InputProps`
104
+ - `inputProps`
105
+ - `SelectProps`
106
+ - `InputLabelProps`
107
+ - `FormHelperTextProps`
108
+ - [tooltip] Remove deprecated props (#47988) @siriwatknp
109
+ - `components`
110
+ - `componentsProps`
111
+ - `PopperComponent`
112
+ - `PopperProps`
113
+ - `TransitionComponent`
114
+ - `TransitionProps`
115
+ - [typography] Remove deprecated prop (#47986) @siriwatknp
116
+ - `paragraph`
117
+ - [buttonbase] Key handlers should not run on a disabled non-native button (#48003) @mj12albert
118
+ - [tablepagination] Format pagination numbers according to locale (#47803) @siriwatknp
119
+ - [textfield] Use non-native label for `<TextField select/>` (#47958) @mj12albert
120
+
121
+ Check out the [v9 upgrade guide](https://mui.com/material-ui/migration/upgrade-to-v9/).
122
+
123
+ #### Changes
124
+
125
+ - [autocomplete] Optimize selected option lookup (#47953) @anchmelev
126
+ - [buttonbase] Fix native button detection (#47985) @mj12albert
127
+ - [inputbase] Fix test using removed InputProps on TextField (#48000) @siriwatknp
128
+ - [popper] Add missing classes export (#48031) @mj12albert
129
+ - [select] Fix focus visible always set on menu item (#47912) @silviuaavram
130
+ - [slider] Fix focus visible styles incorrectly applied by pointer (#47894) @mj12albert
131
+ - [slider] Accept readonly array for the value prop (#47936) @pcorpet
132
+
133
+ ### `@mui/codemod@9.0.0-alpha.4`
134
+
135
+ - [codemod] Add slot key rename to `tabs-props` codemod and add to README (#48035) @siriwatknp
136
+ - [codemod] Add card-header-props migration guide (#48005) @siriwatknp
137
+
138
+ ### Docs
139
+
140
+ - [blog] Lowercase 2026-MUI-X-price-changes blog post URL (#47871) @Janpot
141
+ - [docs] Wide docs layout (#47950) @noraleonte
142
+ - [docs] Mention the list of props removed in upgrade guide (#48010) @silviuaavram
143
+ - [docs] Update Autocomplete deprecated props removal migration guide docs (#47990) @ZeeshanTamboli
144
+ - [docs-infra] Extract shared App component from \_app into @mui/docs (#47933) @brijeshb42
145
+ - [docs-infra] Expose global theme object (#47964) @Janpot
146
+ - [docs][system] Update `styled` API docs for easier understanding (#47957) @olivier-lxo
147
+
148
+ ### Core
149
+
150
+ - [code-infra] Add pkg-pr-new as dev dependency (#47992) @Janpot
151
+ - [code-infra] Remove CI coverage collection and upload to Codecov (#47930) @Janpot
152
+ - [core] Migrate to ESLint 10 (#47872) @brijeshb42
153
+ - [core] Remove GridLegacy component (#47956) @siriwatknp
154
+
155
+ All contributors of this release in alphabetical order: @anchmelev, @atharva3333, @brijeshb42, @Janpot, @mj12albert, @noraleonte, @olivier-lxo, @pcorpet, @silviuaavram, @siriwatknp, @ZeeshanTamboli
156
+
157
+ ## 9.0.0-alpha.3
158
+
159
+ <!-- generated comparing v9.0.0-alpha.2..master -->
160
+
161
+ _Mar 12, 2026_
162
+
163
+ A big thanks to the 10 contributors who made this release possible. Here are some highlights ✨:
164
+
165
+ - 📖 A new [Menubar](https://mui.com/material-ui/react-menubar/) component page integrated with [Base UI](https://base-ui.com/react/components/menubar)
166
+ - ♿️ Improved the Roving TabIndex keyboard navigation for the Stepper, Tabs and MenuList components.
167
+
168
+ ### `@mui/material@9.0.0-alpha.3`
169
+
170
+ - [autocomplete] Add `root` slot (#47852) @GerardasB
171
+ - [autocomplete] Fix popup reopening on window focus regain with openOnFocus (#47790) @aman44444
172
+ - [autocomplete] Support full slots for clearIndicator and popupIndicator (#47891) @silviuaavram
173
+ - [material-ui] Partially revert "[material-ui] Clean up duplicated CSS rules (#47838)" (#47927) @sai6855
174
+ - [stepper][menulist][tabs] Improve accessibility (#47687) @silviuaavram
175
+
176
+ ### Docs
177
+
178
+ - [docs][codemod] Add v7 migration docs for deprecated Autocomplete APIs and Autocomplete codemod (#47945) @ZeeshanTamboli
179
+ - [docs] Update faq about vendor chunks (#47747) @Janpot
180
+ - [docs] Use direct palette vars in Tailwind v4 snippet (#47940) @Ahmad-Alaziz
181
+ - [docs][menubar] Add Menubar component page (#47616) @siriwatknp
182
+
183
+ ### Core
184
+
185
+ - [core] Fix the release prepare steps (#47951) @silviuaavram
186
+ - [core] Remove Joy UI code and docs (#47939) @mnajdova
187
+ - [code-infra] Add previously missed export of themeCssVarsAugmentation (#47918) @brijeshb42
188
+ - [docs-infra] Import font module for nextjs transpilation (#47935) @brijeshb42
189
+ - [docs-infra] Migrate simpler modules from docs to mui-docs (#47897) @brijeshb42
190
+ - [test] Fix detached anchorEl elements in tests (#47929) @Janpot
191
+
192
+ All contributors of this release in alphabetical order: @Ahmad-Alaziz, @aman44444, @brijeshb42, @GerardasB, @Janpot, @mnajdova, @sai6855, @silviuaavram, @siriwatknp, @ZeeshanTamboli
193
+
3
194
  ## 9.0.0-alpha.2
4
195
 
5
196
  <!-- generated comparing v9.0.0-alpha.1..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.2
2
+ * @mui/utils v9.0.0-alpha.4
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.2
2
+ * @mui/utils v9.0.0-alpha.4
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.2",
3
+ "version": "9.0.0-alpha.4",
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.2"
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
+ }