@mui/utils 9.0.0-alpha.2 → 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 +37 -0
- package/index.d.mts +1 -0
- package/index.d.ts +1 -0
- package/index.js +10 -2
- package/index.mjs +2 -1
- package/package.json +16 -2
- package/useRovingTabIndex/index.d.mts +1 -0
- package/useRovingTabIndex/index.d.ts +1 -0
- package/useRovingTabIndex/index.js +13 -0
- package/useRovingTabIndex/index.mjs +1 -0
- package/useRovingTabIndex/useRovingTabIndex.d.mts +31 -0
- package/useRovingTabIndex/useRovingTabIndex.d.ts +31 -0
- package/useRovingTabIndex/useRovingTabIndex.js +197 -0
- package/useRovingTabIndex/useRovingTabIndex.mjs +190 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
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
|
+
|
|
3
40
|
## 9.0.0-alpha.2
|
|
4
41
|
|
|
5
42
|
<!-- 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
|
+
* @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.
|
|
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.
|
|
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.
|
|
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
|
+
}
|