@mui/material 9.0.0-beta.0 → 9.0.0-beta.1

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.
Files changed (138) hide show
  1. package/Accordion/Accordion.d.mts +1 -1
  2. package/Accordion/Accordion.d.ts +1 -1
  3. package/AccordionSummary/AccordionSummary.js +1 -0
  4. package/AccordionSummary/AccordionSummary.mjs +1 -0
  5. package/Backdrop/Backdrop.d.mts +1 -1
  6. package/Backdrop/Backdrop.d.ts +1 -1
  7. package/BottomNavigationAction/BottomNavigationAction.js +1 -0
  8. package/BottomNavigationAction/BottomNavigationAction.mjs +1 -0
  9. package/Breadcrumbs/BreadcrumbCollapsed.js +5 -1
  10. package/Breadcrumbs/BreadcrumbCollapsed.mjs +5 -1
  11. package/Button/Button.js +1 -0
  12. package/Button/Button.mjs +1 -0
  13. package/ButtonBase/ButtonBase.d.mts +5 -0
  14. package/ButtonBase/ButtonBase.d.ts +5 -0
  15. package/ButtonBase/ButtonBase.js +84 -85
  16. package/ButtonBase/ButtonBase.mjs +84 -85
  17. package/ButtonBase/useButtonBase.d.mts +91 -0
  18. package/ButtonBase/useButtonBase.d.ts +91 -0
  19. package/ButtonBase/useButtonBase.js +174 -0
  20. package/ButtonBase/useButtonBase.mjs +167 -0
  21. package/CHANGELOG.md +48 -0
  22. package/CardActionArea/CardActionArea.js +1 -0
  23. package/CardActionArea/CardActionArea.mjs +1 -0
  24. package/Chip/Chip.d.mts +7 -0
  25. package/Chip/Chip.d.ts +7 -0
  26. package/Chip/Chip.js +18 -1
  27. package/Chip/Chip.mjs +18 -1
  28. package/Dialog/Dialog.d.mts +8 -1
  29. package/Dialog/Dialog.d.ts +8 -1
  30. package/Dialog/Dialog.js +9 -1
  31. package/Dialog/Dialog.mjs +9 -1
  32. package/Divider/Divider.js +0 -8
  33. package/Divider/Divider.mjs +0 -8
  34. package/Drawer/Drawer.d.mts +1 -1
  35. package/Drawer/Drawer.d.ts +1 -1
  36. package/Fab/Fab.js +1 -0
  37. package/Fab/Fab.mjs +1 -0
  38. package/FilledInput/FilledInput.js +1 -1
  39. package/FilledInput/FilledInput.mjs +1 -1
  40. package/Grid/Grid.d.mts +8 -3
  41. package/Grid/Grid.d.ts +8 -3
  42. package/Grid/Grid.js +8 -3
  43. package/Grid/Grid.mjs +8 -3
  44. package/Grid/gridClasses.js +1 -1
  45. package/Grid/gridClasses.mjs +1 -1
  46. package/IconButton/IconButton.js +1 -0
  47. package/IconButton/IconButton.mjs +1 -0
  48. package/Input/Input.js +1 -1
  49. package/Input/Input.mjs +1 -1
  50. package/InputAdornment/inputAdornmentClasses.d.mts +2 -2
  51. package/InputAdornment/inputAdornmentClasses.d.ts +2 -2
  52. package/ListItemButton/ListItemButton.js +1 -0
  53. package/ListItemButton/ListItemButton.mjs +1 -0
  54. package/ListItemIcon/ListItemIcon.js +1 -1
  55. package/ListItemIcon/ListItemIcon.mjs +1 -1
  56. package/ListSubheader/ListSubheader.js +0 -3
  57. package/ListSubheader/ListSubheader.mjs +0 -3
  58. package/Menu/Menu.d.mts +1 -1
  59. package/Menu/Menu.d.ts +1 -1
  60. package/Menu/Menu.js +15 -32
  61. package/Menu/Menu.mjs +15 -32
  62. package/MenuItem/MenuItem.js +36 -26
  63. package/MenuItem/MenuItem.mjs +34 -26
  64. package/MenuList/MenuList.js +136 -101
  65. package/MenuList/MenuList.mjs +135 -100
  66. package/MenuList/MenuListContext.d.mts +11 -0
  67. package/MenuList/MenuListContext.d.ts +11 -0
  68. package/MenuList/MenuListContext.js +25 -0
  69. package/MenuList/MenuListContext.mjs +19 -0
  70. package/PaginationItem/PaginationItem.d.mts +5 -0
  71. package/PaginationItem/PaginationItem.d.ts +5 -0
  72. package/PaginationItem/PaginationItem.js +6 -0
  73. package/PaginationItem/PaginationItem.mjs +6 -0
  74. package/PigmentGrid/PigmentGrid.d.mts +1 -1
  75. package/PigmentGrid/PigmentGrid.d.ts +1 -1
  76. package/PigmentGrid/PigmentGrid.js +1 -1
  77. package/PigmentGrid/PigmentGrid.mjs +1 -1
  78. package/Popover/Popover.d.mts +1 -1
  79. package/Popover/Popover.d.ts +1 -1
  80. package/Popover/Popover.js +19 -7
  81. package/Popover/Popover.mjs +18 -6
  82. package/Snackbar/Snackbar.d.mts +1 -1
  83. package/Snackbar/Snackbar.d.ts +1 -1
  84. package/SpeedDial/SpeedDial.d.mts +1 -1
  85. package/SpeedDial/SpeedDial.d.ts +1 -1
  86. package/StepButton/StepButton.js +44 -14
  87. package/StepButton/StepButton.mjs +44 -14
  88. package/StepContent/StepContent.d.mts +1 -1
  89. package/StepContent/StepContent.d.ts +1 -1
  90. package/Stepper/Stepper.js +54 -22
  91. package/Stepper/Stepper.mjs +54 -22
  92. package/Stepper/StepperContext.d.mts +0 -5
  93. package/Stepper/StepperContext.d.ts +0 -5
  94. package/Stepper/StepperContext.js +1 -2
  95. package/Stepper/StepperContext.mjs +0 -1
  96. package/Tab/Tab.js +17 -1
  97. package/Tab/Tab.mjs +17 -1
  98. package/TabScrollButton/TabScrollButton.d.mts +1 -1
  99. package/TabScrollButton/TabScrollButton.d.ts +1 -1
  100. package/TabScrollButton/TabScrollButton.js +6 -2
  101. package/TabScrollButton/TabScrollButton.mjs +6 -2
  102. package/TableSortLabel/TableSortLabel.js +4 -1
  103. package/TableSortLabel/TableSortLabel.mjs +4 -1
  104. package/Tabs/Tabs.js +30 -21
  105. package/Tabs/Tabs.mjs +29 -20
  106. package/ToggleButton/ToggleButton.js +1 -0
  107. package/ToggleButton/ToggleButton.mjs +1 -0
  108. package/Tooltip/Tooltip.d.mts +1 -1
  109. package/Tooltip/Tooltip.d.ts +1 -1
  110. package/index.js +1 -1
  111. package/index.mjs +1 -1
  112. package/internal/SwitchBase.d.mts +2 -2
  113. package/internal/SwitchBase.d.ts +2 -2
  114. package/internal/SwitchBase.js +5 -1
  115. package/internal/SwitchBase.mjs +5 -1
  116. package/locale/psAF.js +1 -1
  117. package/locale/psAF.mjs +1 -1
  118. package/package.json +5 -5
  119. package/styles/createThemeWithVars.js +9 -9
  120. package/styles/createThemeWithVars.mjs +9 -9
  121. package/useAutocomplete/useAutocomplete.js +8 -0
  122. package/useAutocomplete/useAutocomplete.mjs +8 -0
  123. package/utils/focusWithVisible.js +24 -0
  124. package/utils/focusWithVisible.mjs +19 -0
  125. package/utils/index.d.mts +0 -1
  126. package/utils/index.d.ts +0 -1
  127. package/utils/index.js +0 -7
  128. package/utils/index.mjs +0 -1
  129. package/utils/useFocusableWhenDisabled.d.mts +30 -0
  130. package/utils/useFocusableWhenDisabled.d.ts +30 -0
  131. package/utils/useFocusableWhenDisabled.js +47 -0
  132. package/utils/useFocusableWhenDisabled.mjs +41 -0
  133. package/utils/useRovingTabIndex.d.mts +1 -2
  134. package/utils/useRovingTabIndex.d.ts +1 -2
  135. package/utils/useRovingTabIndex.js +25 -4
  136. package/utils/useRovingTabIndex.mjs +1 -2
  137. package/version/index.js +2 -2
  138. package/version/index.mjs +2 -2
@@ -8,26 +8,41 @@ Object.defineProperty(exports, "__esModule", {
8
8
  });
9
9
  exports.default = void 0;
10
10
  var React = _interopRequireWildcard(require("react"));
11
- var _reactIs = require("react-is");
12
11
  var _propTypes = _interopRequireDefault(require("prop-types"));
13
- var _useRovingTabIndex = _interopRequireDefault(require("../utils/useRovingTabIndex"));
12
+ var _useRovingTabIndex = require("@mui/utils/useRovingTabIndex");
14
13
  var _ownerDocument = _interopRequireDefault(require("../utils/ownerDocument"));
15
- var _List = _interopRequireDefault(require("../List"));
16
14
  var _getActiveElement = _interopRequireDefault(require("../utils/getActiveElement"));
17
15
  var _getScrollbarSize = _interopRequireDefault(require("../utils/getScrollbarSize"));
16
+ var _focusWithVisible = _interopRequireDefault(require("../utils/focusWithVisible"));
17
+ var _useEventCallback = _interopRequireDefault(require("../utils/useEventCallback"));
18
18
  var _useForkRef = _interopRequireDefault(require("../utils/useForkRef"));
19
19
  var _useEnhancedEffect = _interopRequireDefault(require("../utils/useEnhancedEffect"));
20
- var _utils = require("../utils");
20
+ var _useRovingTabIndex2 = require("../utils/useRovingTabIndex");
21
+ var _ownerWindow = _interopRequireDefault(require("../utils/ownerWindow"));
22
+ var _List = _interopRequireDefault(require("../List"));
23
+ var _utils = require("../Select/utils");
24
+ var _MenuListContext = require("./MenuListContext");
21
25
  var _jsxRuntime = require("react/jsx-runtime");
22
- function textCriteriaMatches(nextFocus, textCriteria) {
23
- if (textCriteria === undefined) {
24
- return true;
26
+ function getItemText(itemOrElement) {
27
+ const element = itemOrElement?.element ?? itemOrElement;
28
+ if (!element) {
29
+ return '';
25
30
  }
26
- let text = nextFocus.innerText;
31
+ if (itemOrElement?.textValue !== undefined) {
32
+ return itemOrElement.textValue;
33
+ }
34
+ let text = element.innerText;
27
35
  if (text === undefined) {
28
36
  // jsdom doesn't support innerText
29
- text = nextFocus.textContent;
37
+ text = element.textContent;
30
38
  }
39
+ return text ?? '';
40
+ }
41
+ function textCriteriaMatches(itemOrElement, textCriteria) {
42
+ if (textCriteria === undefined) {
43
+ return true;
44
+ }
45
+ let text = getItemText(itemOrElement);
31
46
  text = text.trim().toLowerCase();
32
47
  if (text.length === 0) {
33
48
  return false;
@@ -37,20 +52,17 @@ function textCriteriaMatches(nextFocus, textCriteria) {
37
52
  }
38
53
  return text.startsWith(textCriteria.keys.join(''));
39
54
  }
40
- function shouldFocusWithTextCriteria(element, criteria, disabledItemsFocusable) {
41
- if (!textCriteriaMatches(element, criteria)) {
55
+ function isItemFocusableWithTextCriteria(item, criteria) {
56
+ if (!textCriteriaMatches(item, criteria)) {
42
57
  return false;
43
58
  }
44
- return shouldFocus(element, disabledItemsFocusable);
59
+ return (0, _useRovingTabIndex.isItemFocusable)(item);
45
60
  }
46
- function shouldFocus(element, disabledItemsFocusable) {
47
- if (!element || !element.hasAttribute('tabindex')) {
48
- return false;
49
- }
50
- if (disabledItemsFocusable) {
51
- return true;
52
- }
53
- return !element.disabled && element.getAttribute('aria-disabled') !== 'true';
61
+
62
+ // Menu auto-focus is not always keyboard-driven. On open we often move focus to the
63
+ // active item programmatically so arrow-key navigation starts from the right place.
64
+ function focusInitialItem(element, focusSource) {
65
+ (0, _focusWithVisible.default)(element, focusSource);
54
66
  }
55
67
 
56
68
  /**
@@ -64,8 +76,8 @@ const MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
64
76
  // private
65
77
  // eslint-disable-next-line react/prop-types
66
78
  actions,
67
- autoFocus = false,
68
- autoFocusItem = false,
79
+ autoFocus: autoFocusList = false,
80
+ autoFocusItem: autoFocusActiveItem = false,
69
81
  children,
70
82
  className,
71
83
  disabledItemsFocusable = false,
@@ -75,17 +87,85 @@ const MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
75
87
  ...other
76
88
  } = props;
77
89
  const listRef = React.useRef(null);
90
+ const hasFocusedInitialTargetRef = React.useRef(false);
91
+ // Escape hatch for <Menu variant="menu"> (items have no selection state). When opened with
92
+ // mouse/pointer, the initial focused item should still receive DOM focus, but ButtonBase
93
+ // should suppress its focus-visible state for that one initial handoff.
94
+ const [suppressInitialFocusVisible, setSuppressInitialFocusVisible] = React.useState(false);
95
+ // Current anchored <Menu>s cannot receive a `openInteractionType` signal from a trigger
96
+ // the API only receives `open` and `anchorEl`. When <MenuList> is used in <Select>, the
97
+ // internal <SelectInput> is able to achieve this via `useSelectFocusSource`.
98
+ const focusSource = (0, _utils.useSelectFocusSource)();
78
99
  const textCriteriaRef = React.useRef({
79
100
  keys: [],
80
101
  repeating: true,
81
102
  previousKeyMatched: true,
82
103
  lastTime: null
83
104
  });
84
- (0, _useEnhancedEffect.default)(() => {
85
- if (autoFocus) {
105
+ const getDefaultActiveItemId = React.useCallback(items => {
106
+ if (variant === 'selectedMenu') {
107
+ return items.find(item => item.selected && (0, _useRovingTabIndex.isItemFocusable)(item))?.id ?? items.find(item => (0, _useRovingTabIndex.isItemFocusable)(item))?.id ?? null;
108
+ }
109
+ return items.find(item => (0, _useRovingTabIndex.isItemFocusable)(item))?.id ?? null;
110
+ }, [variant]);
111
+ const rovingContainer = (0, _useRovingTabIndex2.useRovingTabIndexRoot)({
112
+ activeItemId: undefined,
113
+ getDefaultActiveItemId,
114
+ orientation: 'vertical',
115
+ wrap: !disableListWrap
116
+ });
117
+ const {
118
+ activeItemId,
119
+ focusNext,
120
+ getActiveItem,
121
+ getContainerProps,
122
+ getItemMap
123
+ } = rovingContainer;
124
+ const focusInitialTarget = (0, _useEventCallback.default)((force = false) => {
125
+ // `force` is used by the imperative action when `Menu` asks `MenuList` to restore its
126
+ // initial focus target after the popover finishes entering, even if this list already
127
+ // completed its normal one-time initial-focus path on an earlier render.
128
+ if (!listRef.current || !force && hasFocusedInitialTargetRef.current) {
129
+ return null;
130
+ }
131
+ if (autoFocusActiveItem) {
132
+ const activeItem = getActiveItem();
133
+ if (activeItem?.element) {
134
+ const hasSelectedItem = Array.from(getItemMap().values()).some(item => item.selected);
135
+ const shouldSuppressInitialFocusVisible = variant === 'menu' && hasSelectedItem && !activeItem.selected && focusSource == null;
136
+ setSuppressInitialFocusVisible(shouldSuppressInitialFocusVisible);
137
+ focusInitialItem(activeItem.element, focusSource);
138
+ hasFocusedInitialTargetRef.current = true;
139
+ return activeItem.element;
140
+ }
141
+ if (!autoFocusList) {
142
+ return null;
143
+ }
144
+
145
+ // Keep the list container focusable while waiting for items to register,
146
+ // or when there is no focusable item to move to.
147
+ setSuppressInitialFocusVisible(false);
86
148
  listRef.current.focus();
149
+ return listRef.current;
87
150
  }
88
- }, [autoFocus]);
151
+ if (!autoFocusList) {
152
+ setSuppressInitialFocusVisible(false);
153
+ return null;
154
+ }
155
+ setSuppressInitialFocusVisible(false);
156
+ listRef.current.focus();
157
+ hasFocusedInitialTargetRef.current = true;
158
+ return listRef.current;
159
+ });
160
+ (0, _useEnhancedEffect.default)(() => {
161
+ if (!autoFocusList && !autoFocusActiveItem) {
162
+ hasFocusedInitialTargetRef.current = false;
163
+ setSuppressInitialFocusVisible(false);
164
+ return undefined;
165
+ }
166
+ focusInitialTarget();
167
+ return undefined;
168
+ }, [activeItemId, autoFocusActiveItem, autoFocusList, focusInitialTarget]);
89
169
  React.useImperativeHandle(actions, () => ({
90
170
  adjustStyleForScrollbar: (containerElement, {
91
171
  direction
@@ -94,91 +174,40 @@ const MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
94
174
  // of the menu.
95
175
  const noExplicitWidth = !listRef.current.style.width;
96
176
  if (containerElement.clientHeight < listRef.current.clientHeight && noExplicitWidth) {
97
- const scrollbarSize = `${(0, _getScrollbarSize.default)((0, _utils.ownerWindow)(containerElement))}px`;
177
+ const scrollbarSize = `${(0, _getScrollbarSize.default)((0, _ownerWindow.default)(containerElement))}px`;
98
178
  listRef.current.style[direction === 'rtl' ? 'paddingLeft' : 'paddingRight'] = scrollbarSize;
99
179
  listRef.current.style.width = `calc(100% + ${scrollbarSize})`;
100
180
  }
101
181
  return listRef.current;
102
- }
103
- }), []);
104
-
105
- /**
106
- * the index of the item should receive focus
107
- * in a `variant="selectedMenu"` it's the first `selected` item
108
- * otherwise it's the very first item.
109
- */
110
- let activeItemIndex = -1;
111
- // since we inject focus related props into children we have to do a lookahead
112
- // to check if there is a `selected` item. We're looking for the last `selected`
113
- // item and use the first valid item as a fallback
114
- React.Children.forEach(children, (child, index) => {
115
- if (! /*#__PURE__*/React.isValidElement(child)) {
116
- if (activeItemIndex === index) {
117
- activeItemIndex += 1;
118
- if (activeItemIndex >= children.length) {
119
- // there are no focusable items within the list.
120
- activeItemIndex = -1;
121
- }
122
- }
123
- return;
124
- }
125
- if (process.env.NODE_ENV !== 'production') {
126
- if ((0, _reactIs.isFragment)(child)) {
127
- console.error(["MUI: The Menu component doesn't accept a Fragment as a child.", 'Consider providing an array instead.'].join('\n'));
128
- }
129
- }
130
- if (!child.props.disabled) {
131
- if (variant === 'selectedMenu' && child.props.selected) {
132
- activeItemIndex = index;
133
- } else if (activeItemIndex === -1) {
134
- activeItemIndex = index;
182
+ },
183
+ focusInitialTarget: () => {
184
+ if (!listRef.current) {
185
+ return null;
135
186
  }
136
- }
137
- if (activeItemIndex === index && (child.props.disabled || child.props.muiSkipListHighlight || child.type.muiSkipListHighlight)) {
138
- activeItemIndex += 1;
139
- if (activeItemIndex >= children.length) {
140
- // there are no focusable items within the list.
141
- activeItemIndex = -1;
187
+ const currentFocus = (0, _getActiveElement.default)((0, _ownerDocument.default)(listRef.current));
188
+ if (currentFocus && listRef.current.contains(currentFocus)) {
189
+ return currentFocus;
142
190
  }
191
+ return focusInitialTarget(true);
143
192
  }
144
- });
145
- const {
146
- focusNext,
147
- getContainerProps,
148
- getItemProps
149
- } = (0, _useRovingTabIndex.default)({
150
- focusableIndex: activeItemIndex,
151
- orientation: 'vertical',
152
- shouldWrap: !disableListWrap,
153
- shouldFocus: element => shouldFocus(element, disabledItemsFocusable)
154
- });
155
- const rovingTabIndexContainerProps = getContainerProps();
156
- const handleRef = (0, _useForkRef.default)(listRef, rovingTabIndexContainerProps.ref, ref);
157
- let focusableIndex = 0;
158
- const items = React.Children.map(children, (child, index) => {
159
- if (! /*#__PURE__*/React.isValidElement(child) || child.props.muiSkipListHighlight || child.type.muiSkipListHighlight) {
160
- return child;
161
- }
162
- const rovingTabIndexItemProps = getItemProps(focusableIndex, child.ref);
163
- const newChildProps = {
164
- ref: rovingTabIndexItemProps.ref
165
- };
166
- if (child.props.tabIndex === undefined && variant === 'selectedMenu') {
167
- newChildProps.tabIndex = rovingTabIndexItemProps.tabIndex;
193
+ }), [focusInitialTarget]);
194
+ const rovingContainerProps = getContainerProps();
195
+ const handleRef = (0, _useForkRef.default)(listRef, rovingContainerProps.ref, ref);
196
+ const menuListContextValue = React.useMemo(() => ({
197
+ itemsFocusableWhenDisabled: disabledItemsFocusable,
198
+ suppressInitialFocusVisible,
199
+ variant
200
+ }), [disabledItemsFocusable, suppressInitialFocusVisible, variant]);
201
+ const handleKeyDown = (0, _useEventCallback.default)(event => {
202
+ if (suppressInitialFocusVisible) {
203
+ setSuppressInitialFocusVisible(false);
168
204
  }
169
- if (index === activeItemIndex && autoFocusItem) {
170
- newChildProps.autoFocus = true;
171
- }
172
- focusableIndex += 1;
173
- return /*#__PURE__*/React.cloneElement(child, newChildProps);
174
- });
175
- const handleKeyDown = event => {
176
205
  const isModifierKeyPressed = event.ctrlKey || event.metaKey || event.altKey;
177
206
  if (isModifierKeyPressed && onKeyDown) {
178
207
  onKeyDown(event);
179
208
  return;
180
209
  }
181
- rovingTabIndexContainerProps.onKeyDown(event);
210
+ rovingContainerProps.onKeyDown(event);
182
211
  if (event.key.length === 1) {
183
212
  const criteria = textCriteriaRef.current;
184
213
  const lowerKey = event.key.toLowerCase();
@@ -197,7 +226,7 @@ const MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
197
226
  criteria.keys.push(lowerKey);
198
227
  const currentFocus = (0, _getActiveElement.default)((0, _ownerDocument.default)(listRef.current));
199
228
  const keepFocusOnCurrent = currentFocus && !criteria.repeating && textCriteriaMatches(currentFocus, criteria);
200
- if (criteria.previousKeyMatched && (keepFocusOnCurrent || focusNext(element => shouldFocusWithTextCriteria(element, criteria, disabledItemsFocusable)) !== -1)) {
229
+ if (criteria.previousKeyMatched && (keepFocusOnCurrent || focusNext(item => isItemFocusableWithTextCriteria(item, criteria)) != null)) {
201
230
  event.preventDefault();
202
231
  } else {
203
232
  criteria.previousKeyMatched = false;
@@ -206,16 +235,22 @@ const MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
206
235
  if (onKeyDown) {
207
236
  onKeyDown(event);
208
237
  }
209
- };
238
+ });
210
239
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_List.default, {
211
240
  role: "menu",
212
241
  ref: handleRef,
213
242
  className: className,
214
243
  onKeyDown: handleKeyDown,
215
- onFocus: rovingTabIndexContainerProps.onFocus,
244
+ onFocus: rovingContainerProps.onFocus,
216
245
  tabIndex: -1,
217
246
  ...other,
218
- children: items
247
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_MenuListContext.MenuListContext.Provider, {
248
+ value: menuListContextValue,
249
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_useRovingTabIndex2.RovingTabIndexContext.Provider, {
250
+ value: rovingContainer,
251
+ children: children
252
+ })
253
+ })
219
254
  });
220
255
  });
221
256
  process.env.NODE_ENV !== "production" ? MenuList.propTypes /* remove-proptypes */ = {
@@ -1,26 +1,41 @@
1
1
  'use client';
2
2
 
3
3
  import * as React from 'react';
4
- import { isFragment } from 'react-is';
5
4
  import PropTypes from 'prop-types';
6
- import useRovingTabIndex from "../utils/useRovingTabIndex.mjs";
5
+ import { isItemFocusable } from '@mui/utils/useRovingTabIndex';
7
6
  import ownerDocument from "../utils/ownerDocument.mjs";
8
- import List from "../List/index.mjs";
9
7
  import getActiveElement from "../utils/getActiveElement.mjs";
10
8
  import getScrollbarSize from "../utils/getScrollbarSize.mjs";
9
+ import focusWithVisible from "../utils/focusWithVisible.mjs";
10
+ import useEventCallback from "../utils/useEventCallback.mjs";
11
11
  import useForkRef from "../utils/useForkRef.mjs";
12
12
  import useEnhancedEffect from "../utils/useEnhancedEffect.mjs";
13
- import { ownerWindow } from "../utils/index.mjs";
13
+ import { RovingTabIndexContext, useRovingTabIndexRoot } from "../utils/useRovingTabIndex.mjs";
14
+ import ownerWindow from "../utils/ownerWindow.mjs";
15
+ import List from "../List/index.mjs";
16
+ import { useSelectFocusSource } from "../Select/utils/index.mjs";
17
+ import { MenuListContext } from "./MenuListContext.mjs";
14
18
  import { jsx as _jsx } from "react/jsx-runtime";
15
- function textCriteriaMatches(nextFocus, textCriteria) {
16
- if (textCriteria === undefined) {
17
- return true;
19
+ function getItemText(itemOrElement) {
20
+ const element = itemOrElement?.element ?? itemOrElement;
21
+ if (!element) {
22
+ return '';
18
23
  }
19
- let text = nextFocus.innerText;
24
+ if (itemOrElement?.textValue !== undefined) {
25
+ return itemOrElement.textValue;
26
+ }
27
+ let text = element.innerText;
20
28
  if (text === undefined) {
21
29
  // jsdom doesn't support innerText
22
- text = nextFocus.textContent;
30
+ text = element.textContent;
23
31
  }
32
+ return text ?? '';
33
+ }
34
+ function textCriteriaMatches(itemOrElement, textCriteria) {
35
+ if (textCriteria === undefined) {
36
+ return true;
37
+ }
38
+ let text = getItemText(itemOrElement);
24
39
  text = text.trim().toLowerCase();
25
40
  if (text.length === 0) {
26
41
  return false;
@@ -30,20 +45,17 @@ function textCriteriaMatches(nextFocus, textCriteria) {
30
45
  }
31
46
  return text.startsWith(textCriteria.keys.join(''));
32
47
  }
33
- function shouldFocusWithTextCriteria(element, criteria, disabledItemsFocusable) {
34
- if (!textCriteriaMatches(element, criteria)) {
48
+ function isItemFocusableWithTextCriteria(item, criteria) {
49
+ if (!textCriteriaMatches(item, criteria)) {
35
50
  return false;
36
51
  }
37
- return shouldFocus(element, disabledItemsFocusable);
52
+ return isItemFocusable(item);
38
53
  }
39
- function shouldFocus(element, disabledItemsFocusable) {
40
- if (!element || !element.hasAttribute('tabindex')) {
41
- return false;
42
- }
43
- if (disabledItemsFocusable) {
44
- return true;
45
- }
46
- return !element.disabled && element.getAttribute('aria-disabled') !== 'true';
54
+
55
+ // Menu auto-focus is not always keyboard-driven. On open we often move focus to the
56
+ // active item programmatically so arrow-key navigation starts from the right place.
57
+ function focusInitialItem(element, focusSource) {
58
+ focusWithVisible(element, focusSource);
47
59
  }
48
60
 
49
61
  /**
@@ -57,8 +69,8 @@ const MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
57
69
  // private
58
70
  // eslint-disable-next-line react/prop-types
59
71
  actions,
60
- autoFocus = false,
61
- autoFocusItem = false,
72
+ autoFocus: autoFocusList = false,
73
+ autoFocusItem: autoFocusActiveItem = false,
62
74
  children,
63
75
  className,
64
76
  disabledItemsFocusable = false,
@@ -68,17 +80,85 @@ const MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
68
80
  ...other
69
81
  } = props;
70
82
  const listRef = React.useRef(null);
83
+ const hasFocusedInitialTargetRef = React.useRef(false);
84
+ // Escape hatch for <Menu variant="menu"> (items have no selection state). When opened with
85
+ // mouse/pointer, the initial focused item should still receive DOM focus, but ButtonBase
86
+ // should suppress its focus-visible state for that one initial handoff.
87
+ const [suppressInitialFocusVisible, setSuppressInitialFocusVisible] = React.useState(false);
88
+ // Current anchored <Menu>s cannot receive a `openInteractionType` signal from a trigger
89
+ // the API only receives `open` and `anchorEl`. When <MenuList> is used in <Select>, the
90
+ // internal <SelectInput> is able to achieve this via `useSelectFocusSource`.
91
+ const focusSource = useSelectFocusSource();
71
92
  const textCriteriaRef = React.useRef({
72
93
  keys: [],
73
94
  repeating: true,
74
95
  previousKeyMatched: true,
75
96
  lastTime: null
76
97
  });
77
- useEnhancedEffect(() => {
78
- if (autoFocus) {
98
+ const getDefaultActiveItemId = React.useCallback(items => {
99
+ if (variant === 'selectedMenu') {
100
+ return items.find(item => item.selected && isItemFocusable(item))?.id ?? items.find(item => isItemFocusable(item))?.id ?? null;
101
+ }
102
+ return items.find(item => isItemFocusable(item))?.id ?? null;
103
+ }, [variant]);
104
+ const rovingContainer = useRovingTabIndexRoot({
105
+ activeItemId: undefined,
106
+ getDefaultActiveItemId,
107
+ orientation: 'vertical',
108
+ wrap: !disableListWrap
109
+ });
110
+ const {
111
+ activeItemId,
112
+ focusNext,
113
+ getActiveItem,
114
+ getContainerProps,
115
+ getItemMap
116
+ } = rovingContainer;
117
+ const focusInitialTarget = useEventCallback((force = false) => {
118
+ // `force` is used by the imperative action when `Menu` asks `MenuList` to restore its
119
+ // initial focus target after the popover finishes entering, even if this list already
120
+ // completed its normal one-time initial-focus path on an earlier render.
121
+ if (!listRef.current || !force && hasFocusedInitialTargetRef.current) {
122
+ return null;
123
+ }
124
+ if (autoFocusActiveItem) {
125
+ const activeItem = getActiveItem();
126
+ if (activeItem?.element) {
127
+ const hasSelectedItem = Array.from(getItemMap().values()).some(item => item.selected);
128
+ const shouldSuppressInitialFocusVisible = variant === 'menu' && hasSelectedItem && !activeItem.selected && focusSource == null;
129
+ setSuppressInitialFocusVisible(shouldSuppressInitialFocusVisible);
130
+ focusInitialItem(activeItem.element, focusSource);
131
+ hasFocusedInitialTargetRef.current = true;
132
+ return activeItem.element;
133
+ }
134
+ if (!autoFocusList) {
135
+ return null;
136
+ }
137
+
138
+ // Keep the list container focusable while waiting for items to register,
139
+ // or when there is no focusable item to move to.
140
+ setSuppressInitialFocusVisible(false);
79
141
  listRef.current.focus();
142
+ return listRef.current;
80
143
  }
81
- }, [autoFocus]);
144
+ if (!autoFocusList) {
145
+ setSuppressInitialFocusVisible(false);
146
+ return null;
147
+ }
148
+ setSuppressInitialFocusVisible(false);
149
+ listRef.current.focus();
150
+ hasFocusedInitialTargetRef.current = true;
151
+ return listRef.current;
152
+ });
153
+ useEnhancedEffect(() => {
154
+ if (!autoFocusList && !autoFocusActiveItem) {
155
+ hasFocusedInitialTargetRef.current = false;
156
+ setSuppressInitialFocusVisible(false);
157
+ return undefined;
158
+ }
159
+ focusInitialTarget();
160
+ return undefined;
161
+ }, [activeItemId, autoFocusActiveItem, autoFocusList, focusInitialTarget]);
82
162
  React.useImperativeHandle(actions, () => ({
83
163
  adjustStyleForScrollbar: (containerElement, {
84
164
  direction
@@ -92,86 +172,35 @@ const MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
92
172
  listRef.current.style.width = `calc(100% + ${scrollbarSize})`;
93
173
  }
94
174
  return listRef.current;
95
- }
96
- }), []);
97
-
98
- /**
99
- * the index of the item should receive focus
100
- * in a `variant="selectedMenu"` it's the first `selected` item
101
- * otherwise it's the very first item.
102
- */
103
- let activeItemIndex = -1;
104
- // since we inject focus related props into children we have to do a lookahead
105
- // to check if there is a `selected` item. We're looking for the last `selected`
106
- // item and use the first valid item as a fallback
107
- React.Children.forEach(children, (child, index) => {
108
- if (! /*#__PURE__*/React.isValidElement(child)) {
109
- if (activeItemIndex === index) {
110
- activeItemIndex += 1;
111
- if (activeItemIndex >= children.length) {
112
- // there are no focusable items within the list.
113
- activeItemIndex = -1;
114
- }
115
- }
116
- return;
117
- }
118
- if (process.env.NODE_ENV !== 'production') {
119
- if (isFragment(child)) {
120
- console.error(["MUI: The Menu component doesn't accept a Fragment as a child.", 'Consider providing an array instead.'].join('\n'));
121
- }
122
- }
123
- if (!child.props.disabled) {
124
- if (variant === 'selectedMenu' && child.props.selected) {
125
- activeItemIndex = index;
126
- } else if (activeItemIndex === -1) {
127
- activeItemIndex = index;
175
+ },
176
+ focusInitialTarget: () => {
177
+ if (!listRef.current) {
178
+ return null;
128
179
  }
129
- }
130
- if (activeItemIndex === index && (child.props.disabled || child.props.muiSkipListHighlight || child.type.muiSkipListHighlight)) {
131
- activeItemIndex += 1;
132
- if (activeItemIndex >= children.length) {
133
- // there are no focusable items within the list.
134
- activeItemIndex = -1;
180
+ const currentFocus = getActiveElement(ownerDocument(listRef.current));
181
+ if (currentFocus && listRef.current.contains(currentFocus)) {
182
+ return currentFocus;
135
183
  }
184
+ return focusInitialTarget(true);
136
185
  }
137
- });
138
- const {
139
- focusNext,
140
- getContainerProps,
141
- getItemProps
142
- } = useRovingTabIndex({
143
- focusableIndex: activeItemIndex,
144
- orientation: 'vertical',
145
- shouldWrap: !disableListWrap,
146
- shouldFocus: element => shouldFocus(element, disabledItemsFocusable)
147
- });
148
- const rovingTabIndexContainerProps = getContainerProps();
149
- const handleRef = useForkRef(listRef, rovingTabIndexContainerProps.ref, ref);
150
- let focusableIndex = 0;
151
- const items = React.Children.map(children, (child, index) => {
152
- if (! /*#__PURE__*/React.isValidElement(child) || child.props.muiSkipListHighlight || child.type.muiSkipListHighlight) {
153
- return child;
154
- }
155
- const rovingTabIndexItemProps = getItemProps(focusableIndex, child.ref);
156
- const newChildProps = {
157
- ref: rovingTabIndexItemProps.ref
158
- };
159
- if (child.props.tabIndex === undefined && variant === 'selectedMenu') {
160
- newChildProps.tabIndex = rovingTabIndexItemProps.tabIndex;
186
+ }), [focusInitialTarget]);
187
+ const rovingContainerProps = getContainerProps();
188
+ const handleRef = useForkRef(listRef, rovingContainerProps.ref, ref);
189
+ const menuListContextValue = React.useMemo(() => ({
190
+ itemsFocusableWhenDisabled: disabledItemsFocusable,
191
+ suppressInitialFocusVisible,
192
+ variant
193
+ }), [disabledItemsFocusable, suppressInitialFocusVisible, variant]);
194
+ const handleKeyDown = useEventCallback(event => {
195
+ if (suppressInitialFocusVisible) {
196
+ setSuppressInitialFocusVisible(false);
161
197
  }
162
- if (index === activeItemIndex && autoFocusItem) {
163
- newChildProps.autoFocus = true;
164
- }
165
- focusableIndex += 1;
166
- return /*#__PURE__*/React.cloneElement(child, newChildProps);
167
- });
168
- const handleKeyDown = event => {
169
198
  const isModifierKeyPressed = event.ctrlKey || event.metaKey || event.altKey;
170
199
  if (isModifierKeyPressed && onKeyDown) {
171
200
  onKeyDown(event);
172
201
  return;
173
202
  }
174
- rovingTabIndexContainerProps.onKeyDown(event);
203
+ rovingContainerProps.onKeyDown(event);
175
204
  if (event.key.length === 1) {
176
205
  const criteria = textCriteriaRef.current;
177
206
  const lowerKey = event.key.toLowerCase();
@@ -190,7 +219,7 @@ const MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
190
219
  criteria.keys.push(lowerKey);
191
220
  const currentFocus = getActiveElement(ownerDocument(listRef.current));
192
221
  const keepFocusOnCurrent = currentFocus && !criteria.repeating && textCriteriaMatches(currentFocus, criteria);
193
- if (criteria.previousKeyMatched && (keepFocusOnCurrent || focusNext(element => shouldFocusWithTextCriteria(element, criteria, disabledItemsFocusable)) !== -1)) {
222
+ if (criteria.previousKeyMatched && (keepFocusOnCurrent || focusNext(item => isItemFocusableWithTextCriteria(item, criteria)) != null)) {
194
223
  event.preventDefault();
195
224
  } else {
196
225
  criteria.previousKeyMatched = false;
@@ -199,16 +228,22 @@ const MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
199
228
  if (onKeyDown) {
200
229
  onKeyDown(event);
201
230
  }
202
- };
231
+ });
203
232
  return /*#__PURE__*/_jsx(List, {
204
233
  role: "menu",
205
234
  ref: handleRef,
206
235
  className: className,
207
236
  onKeyDown: handleKeyDown,
208
- onFocus: rovingTabIndexContainerProps.onFocus,
237
+ onFocus: rovingContainerProps.onFocus,
209
238
  tabIndex: -1,
210
239
  ...other,
211
- children: items
240
+ children: /*#__PURE__*/_jsx(MenuListContext.Provider, {
241
+ value: menuListContextValue,
242
+ children: /*#__PURE__*/_jsx(RovingTabIndexContext.Provider, {
243
+ value: rovingContainer,
244
+ children: children
245
+ })
246
+ })
212
247
  });
213
248
  });
214
249
  process.env.NODE_ENV !== "production" ? MenuList.propTypes /* remove-proptypes */ = {
@@ -0,0 +1,11 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * @ignore - internal component.
4
+ */
5
+ export interface MenuListContextValue {
6
+ itemsFocusableWhenDisabled: boolean;
7
+ suppressInitialFocusVisible: boolean;
8
+ variant: 'menu' | 'selectedMenu';
9
+ }
10
+ export declare const MenuListContext: React.Context<MenuListContextValue | undefined>;
11
+ export declare function useMenuListContext(): MenuListContextValue;
@@ -0,0 +1,11 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * @ignore - internal component.
4
+ */
5
+ export interface MenuListContextValue {
6
+ itemsFocusableWhenDisabled: boolean;
7
+ suppressInitialFocusVisible: boolean;
8
+ variant: 'menu' | 'selectedMenu';
9
+ }
10
+ export declare const MenuListContext: React.Context<MenuListContextValue | undefined>;
11
+ export declare function useMenuListContext(): MenuListContextValue;