@mui/material 9.0.0 → 9.1.0

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 (278) hide show
  1. package/Accordion/Accordion.d.mts +2 -2
  2. package/Accordion/Accordion.d.ts +2 -2
  3. package/Accordion/Accordion.js +3 -2
  4. package/Accordion/Accordion.mjs +3 -2
  5. package/AccordionSummary/AccordionSummary.js +27 -29
  6. package/AccordionSummary/AccordionSummary.mjs +27 -29
  7. package/Autocomplete/Autocomplete.js +73 -17
  8. package/Autocomplete/Autocomplete.mjs +73 -17
  9. package/Avatar/Avatar.js +4 -0
  10. package/Avatar/Avatar.mjs +4 -0
  11. package/Backdrop/Backdrop.d.mts +2 -2
  12. package/Backdrop/Backdrop.d.ts +2 -2
  13. package/Badge/Badge.js +31 -24
  14. package/Badge/Badge.mjs +31 -24
  15. package/BottomNavigationAction/BottomNavigationAction.js +6 -2
  16. package/BottomNavigationAction/BottomNavigationAction.mjs +6 -2
  17. package/Button/Button.js +19 -6
  18. package/Button/Button.mjs +19 -6
  19. package/ButtonBase/ButtonBase.d.mts +7 -0
  20. package/ButtonBase/ButtonBase.d.ts +7 -0
  21. package/ButtonBase/ButtonBase.js +5 -2
  22. package/ButtonBase/ButtonBase.mjs +5 -2
  23. package/ButtonBase/Ripple.js +21 -11
  24. package/ButtonBase/Ripple.mjs +21 -11
  25. package/ButtonBase/TouchRipple.js +252 -116
  26. package/ButtonBase/TouchRipple.mjs +253 -117
  27. package/CHANGELOG.md +216 -1245
  28. package/CardActionArea/CardActionArea.js +2 -1
  29. package/CardActionArea/CardActionArea.mjs +2 -1
  30. package/Checkbox/Checkbox.js +2 -1
  31. package/Checkbox/Checkbox.mjs +2 -1
  32. package/Chip/Chip.js +2 -1
  33. package/Chip/Chip.mjs +2 -1
  34. package/CircularProgress/CircularProgress.d.mts +12 -2
  35. package/CircularProgress/CircularProgress.d.ts +12 -2
  36. package/CircularProgress/CircularProgress.js +115 -58
  37. package/CircularProgress/CircularProgress.mjs +114 -58
  38. package/ClickAwayListener/ClickAwayListener.js +3 -6
  39. package/ClickAwayListener/ClickAwayListener.mjs +3 -6
  40. package/Collapse/Collapse.d.mts +15 -3
  41. package/Collapse/Collapse.d.ts +15 -3
  42. package/Collapse/Collapse.js +44 -31
  43. package/Collapse/Collapse.mjs +43 -30
  44. package/Dialog/Dialog.d.mts +2 -2
  45. package/Dialog/Dialog.d.ts +2 -2
  46. package/Dialog/Dialog.js +13 -6
  47. package/Dialog/Dialog.mjs +13 -6
  48. package/Drawer/Drawer.d.mts +2 -2
  49. package/Drawer/Drawer.d.ts +2 -2
  50. package/Drawer/Drawer.js +18 -4
  51. package/Drawer/Drawer.mjs +18 -4
  52. package/Fab/Fab.js +9 -2
  53. package/Fab/Fab.mjs +9 -2
  54. package/Fade/Fade.d.mts +15 -2
  55. package/Fade/Fade.d.ts +15 -2
  56. package/Fade/Fade.js +46 -19
  57. package/Fade/Fade.mjs +45 -18
  58. package/FilledInput/FilledInput.d.mts +4 -0
  59. package/FilledInput/FilledInput.d.ts +4 -0
  60. package/FilledInput/FilledInput.js +22 -23
  61. package/FilledInput/FilledInput.mjs +22 -23
  62. package/FormControl/useFormControl.d.mts +12 -2
  63. package/FormControl/useFormControl.d.ts +12 -2
  64. package/FormControl/useFormControl.js +13 -0
  65. package/FormControl/useFormControl.mjs +12 -0
  66. package/FormControlLabel/FormControlLabel.js +5 -8
  67. package/FormControlLabel/FormControlLabel.mjs +5 -8
  68. package/FormGroup/FormGroup.js +2 -5
  69. package/FormGroup/FormGroup.mjs +2 -5
  70. package/FormHelperText/FormHelperText.js +2 -5
  71. package/FormHelperText/FormHelperText.mjs +2 -5
  72. package/FormLabel/FormLabel.js +2 -5
  73. package/FormLabel/FormLabel.mjs +2 -5
  74. package/Grow/Grow.d.mts +15 -2
  75. package/Grow/Grow.d.ts +15 -2
  76. package/Grow/Grow.js +45 -28
  77. package/Grow/Grow.mjs +44 -27
  78. package/IconButton/IconButton.js +3 -9
  79. package/IconButton/IconButton.mjs +3 -9
  80. package/Input/Input.d.mts +4 -0
  81. package/Input/Input.d.ts +4 -0
  82. package/Input/Input.js +9 -2
  83. package/Input/Input.mjs +9 -2
  84. package/InputBase/InputBase.d.mts +2 -1
  85. package/InputBase/InputBase.d.ts +2 -1
  86. package/InputBase/InputBase.js +52 -16
  87. package/InputBase/InputBase.mjs +52 -16
  88. package/InputLabel/InputLabel.js +7 -9
  89. package/InputLabel/InputLabel.mjs +7 -9
  90. package/LICENSE +1 -1
  91. package/LinearProgress/LinearProgress.d.mts +12 -2
  92. package/LinearProgress/LinearProgress.d.ts +12 -2
  93. package/LinearProgress/LinearProgress.js +225 -126
  94. package/LinearProgress/LinearProgress.mjs +224 -126
  95. package/List/List.js +2 -1
  96. package/List/List.mjs +2 -1
  97. package/ListItem/ListItem.js +2 -1
  98. package/ListItem/ListItem.mjs +2 -1
  99. package/ListItemButton/ListItemButton.js +9 -2
  100. package/ListItemButton/ListItemButton.mjs +9 -2
  101. package/Menu/Menu.d.mts +1 -1
  102. package/Menu/Menu.d.ts +1 -1
  103. package/MenuItem/MenuItem.js +7 -1
  104. package/MenuItem/MenuItem.mjs +7 -1
  105. package/MenuList/MenuList.js +2 -1
  106. package/MenuList/MenuList.mjs +2 -1
  107. package/MobileStepper/MobileStepper.js +2 -1
  108. package/MobileStepper/MobileStepper.mjs +2 -1
  109. package/NativeSelect/NativeSelect.js +2 -5
  110. package/NativeSelect/NativeSelect.mjs +2 -5
  111. package/OutlinedInput/NotchedOutline.js +4 -3
  112. package/OutlinedInput/NotchedOutline.mjs +4 -3
  113. package/OutlinedInput/OutlinedInput.js +13 -23
  114. package/OutlinedInput/OutlinedInput.mjs +13 -23
  115. package/PaginationItem/PaginationItem.js +2 -1
  116. package/PaginationItem/PaginationItem.mjs +2 -1
  117. package/Paper/Paper.js +2 -1
  118. package/Paper/Paper.mjs +2 -1
  119. package/PigmentContainer/PigmentContainer.js +0 -1
  120. package/PigmentContainer/PigmentContainer.mjs +0 -1
  121. package/Popover/Popover.d.mts +1 -1
  122. package/Popover/Popover.d.ts +1 -1
  123. package/Popper/BasePopper.js +23 -1
  124. package/Popper/BasePopper.mjs +23 -1
  125. package/README.md +3 -2
  126. package/Radio/RadioButtonIcon.js +3 -2
  127. package/Radio/RadioButtonIcon.mjs +3 -2
  128. package/Rating/Rating.js +2 -1
  129. package/Rating/Rating.mjs +2 -1
  130. package/Select/Select.js +2 -5
  131. package/Select/Select.mjs +2 -5
  132. package/Select/SelectInput.js +276 -24
  133. package/Select/SelectInput.mjs +276 -24
  134. package/Select/utils/closedTypeahead.js +73 -0
  135. package/Select/utils/closedTypeahead.mjs +63 -0
  136. package/Skeleton/Skeleton.js +22 -2
  137. package/Skeleton/Skeleton.mjs +22 -2
  138. package/Slide/Slide.d.mts +15 -2
  139. package/Slide/Slide.d.ts +15 -2
  140. package/Slide/Slide.js +97 -47
  141. package/Slide/Slide.mjs +97 -47
  142. package/Slider/Slider.js +14 -4
  143. package/Slider/Slider.mjs +14 -4
  144. package/Slider/useSlider.js +4 -3
  145. package/Slider/useSlider.mjs +4 -3
  146. package/Snackbar/Snackbar.d.mts +2 -2
  147. package/Snackbar/Snackbar.d.ts +2 -2
  148. package/SpeedDial/SpeedDial.d.mts +1 -1
  149. package/SpeedDial/SpeedDial.d.ts +1 -1
  150. package/SpeedDial/SpeedDial.js +6 -2
  151. package/SpeedDial/SpeedDial.mjs +6 -2
  152. package/SpeedDialAction/SpeedDialAction.js +11 -2
  153. package/SpeedDialAction/SpeedDialAction.mjs +12 -3
  154. package/SpeedDialIcon/SpeedDialIcon.js +40 -37
  155. package/SpeedDialIcon/SpeedDialIcon.mjs +40 -37
  156. package/Step/Step.js +47 -15
  157. package/Step/Step.mjs +47 -15
  158. package/StepButton/StepButton.js +9 -3
  159. package/StepButton/StepButton.mjs +9 -3
  160. package/StepConnector/StepConnector.js +10 -0
  161. package/StepConnector/StepConnector.mjs +10 -0
  162. package/StepContent/StepContent.d.mts +2 -2
  163. package/StepContent/StepContent.d.ts +2 -2
  164. package/StepContent/StepContent.js +26 -2
  165. package/StepContent/StepContent.mjs +26 -2
  166. package/StepIcon/StepIcon.js +2 -1
  167. package/StepIcon/StepIcon.mjs +2 -1
  168. package/StepLabel/StepLabel.js +52 -7
  169. package/StepLabel/StepLabel.mjs +52 -7
  170. package/Stepper/Stepper.d.mts +2 -0
  171. package/Stepper/Stepper.d.ts +2 -0
  172. package/Stepper/Stepper.js +18 -0
  173. package/Stepper/Stepper.mjs +18 -0
  174. package/SvgIcon/SvgIcon.js +2 -1
  175. package/SvgIcon/SvgIcon.mjs +2 -1
  176. package/SwipeableDrawer/SwipeableDrawer.js +21 -6
  177. package/SwipeableDrawer/SwipeableDrawer.mjs +21 -6
  178. package/Switch/Switch.js +10 -8
  179. package/Switch/Switch.mjs +10 -8
  180. package/TableSortLabel/TableSortLabel.js +2 -1
  181. package/TableSortLabel/TableSortLabel.mjs +2 -1
  182. package/Tabs/ScrollbarSize.js +2 -1
  183. package/Tabs/ScrollbarSize.mjs +2 -1
  184. package/Tabs/Tabs.js +16 -4
  185. package/Tabs/Tabs.mjs +16 -4
  186. package/Tooltip/Tooltip.d.mts +2 -2
  187. package/Tooltip/Tooltip.d.ts +2 -2
  188. package/Tooltip/Tooltip.js +29 -108
  189. package/Tooltip/Tooltip.mjs +29 -108
  190. package/Unstable_TrapFocus/FocusTrap.js +60 -22
  191. package/Unstable_TrapFocus/FocusTrap.mjs +60 -22
  192. package/Zoom/Zoom.d.mts +15 -2
  193. package/Zoom/Zoom.d.ts +15 -2
  194. package/Zoom/Zoom.js +43 -16
  195. package/Zoom/Zoom.mjs +42 -15
  196. package/index.js +1 -1
  197. package/index.mjs +1 -1
  198. package/internal/Transition.d.mts +34 -0
  199. package/internal/Transition.d.ts +34 -0
  200. package/internal/Transition.js +444 -0
  201. package/internal/Transition.mjs +436 -0
  202. package/internal/react-transition-group.d.mts +8 -0
  203. package/internal/react-transition-group.d.ts +8 -0
  204. package/package.json +50 -50
  205. package/styles/createMotion.d.mts +8 -0
  206. package/styles/createMotion.d.ts +8 -0
  207. package/styles/createMotion.js +13 -0
  208. package/styles/createMotion.mjs +7 -0
  209. package/styles/createThemeFoundation.d.mts +2 -0
  210. package/styles/createThemeFoundation.d.ts +2 -0
  211. package/styles/createThemeNoVars.d.mts +3 -0
  212. package/styles/createThemeNoVars.d.ts +3 -0
  213. package/styles/createThemeNoVars.js +5 -0
  214. package/styles/createThemeNoVars.mjs +5 -0
  215. package/styles/createTransitions.d.mts +6 -2
  216. package/styles/createTransitions.d.ts +6 -2
  217. package/styles/createTransitions.js +12 -4
  218. package/styles/createTransitions.mjs +12 -4
  219. package/styles/enhanceHighContrast.d.mts +70 -0
  220. package/styles/enhanceHighContrast.d.ts +70 -0
  221. package/styles/enhanceHighContrast.js +502 -0
  222. package/styles/enhanceHighContrast.mjs +495 -0
  223. package/styles/index.d.mts +2 -0
  224. package/styles/index.d.ts +2 -0
  225. package/styles/index.js +8 -0
  226. package/styles/index.mjs +1 -0
  227. package/styles/reducedMotion.d.mts +7 -0
  228. package/styles/reducedMotion.d.ts +7 -0
  229. package/styles/reducedMotion.js +21 -0
  230. package/styles/reducedMotion.mjs +14 -0
  231. package/styles/responsiveFontSizes.js +19 -8
  232. package/styles/responsiveFontSizes.mjs +19 -8
  233. package/styles/shouldSkipGeneratingVar.js +1 -1
  234. package/styles/shouldSkipGeneratingVar.mjs +1 -1
  235. package/styles/stringifyTheme.js +1 -0
  236. package/styles/stringifyTheme.mjs +1 -0
  237. package/styles/useThemeProps.d.mts +3 -3
  238. package/styles/useThemeProps.d.ts +3 -3
  239. package/transitions/index.d.mts +1 -1
  240. package/transitions/index.d.ts +1 -1
  241. package/transitions/index.js +0 -11
  242. package/transitions/index.mjs +1 -1
  243. package/transitions/transition.d.mts +1 -12
  244. package/transitions/transition.d.ts +1 -12
  245. package/transitions/types.d.mts +73 -0
  246. package/transitions/types.d.ts +73 -0
  247. package/transitions/useReducedMotion.d.mts +14 -0
  248. package/transitions/useReducedMotion.d.ts +14 -0
  249. package/transitions/useReducedMotion.js +117 -0
  250. package/transitions/useReducedMotion.mjs +110 -0
  251. package/transitions/utils.d.mts +51 -2
  252. package/transitions/utils.d.ts +51 -2
  253. package/transitions/utils.js +97 -4
  254. package/transitions/utils.mjs +94 -4
  255. package/useAutocomplete/useAutocomplete.d.mts +12 -6
  256. package/useAutocomplete/useAutocomplete.d.ts +12 -6
  257. package/useAutocomplete/useAutocomplete.js +230 -55
  258. package/useAutocomplete/useAutocomplete.mjs +230 -55
  259. package/utils/contains.d.mts +2 -0
  260. package/utils/contains.d.ts +2 -0
  261. package/utils/contains.js +9 -0
  262. package/utils/contains.mjs +2 -0
  263. package/utils/focusable.d.mts +7 -0
  264. package/utils/focusable.d.ts +7 -0
  265. package/utils/focusable.js +20 -0
  266. package/utils/focusable.mjs +13 -0
  267. package/utils/getEventTarget.d.mts +2 -0
  268. package/utils/getEventTarget.d.ts +2 -0
  269. package/utils/getEventTarget.js +9 -0
  270. package/utils/getEventTarget.mjs +2 -0
  271. package/utils/mergeSlotProps.js +2 -8
  272. package/utils/mergeSlotProps.mjs +1 -8
  273. package/version/index.js +2 -2
  274. package/version/index.mjs +2 -2
  275. package/FormControl/formControlState.js +0 -21
  276. package/FormControl/formControlState.mjs +0 -15
  277. /package/transitions/{transition.js → types.js} +0 -0
  278. /package/transitions/{transition.mjs → types.mjs} +0 -0
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import * as React from 'react';
4
+ import contains from '@mui/utils/contains';
4
5
  import setRef from '@mui/utils/setRef';
5
6
  import useEventCallback from '@mui/utils/useEventCallback';
6
7
  import useControlled from '@mui/utils/useControlled';
@@ -55,7 +56,7 @@ const defaultFilterOptions = createFilterOptions();
55
56
 
56
57
  // Number of options to jump in list box when `Page Up` and `Page Down` keys are used.
57
58
  const pageSize = 5;
58
- const defaultIsActiveElementInListbox = listboxRef => listboxRef.current !== null && listboxRef.current.parentElement?.contains(document.activeElement);
59
+ const defaultIsActiveElementInListbox = listboxRef => listboxRef.current !== null && contains(listboxRef.current.parentElement, document.activeElement);
59
60
  const defaultIsOptionEqualToValue = (option, value) => option === value;
60
61
  const MULTIPLE_DEFAULT_VALUE = [];
61
62
  function getInputValue(value, multiple, getOptionLabel, renderValue) {
@@ -107,6 +108,7 @@ function useAutocomplete(props) {
107
108
  options,
108
109
  readOnly = false,
109
110
  renderValue,
111
+ resetHighlightOnMouseLeave = false,
110
112
  selectOnFocus = !props.freeSolo,
111
113
  value: valueProp
112
114
  } = props;
@@ -127,12 +129,29 @@ function useAutocomplete(props) {
127
129
  const firstFocus = React.useRef(true);
128
130
  const inputRef = React.useRef(null);
129
131
  const listboxRef = React.useRef(null);
132
+ // VoiceOver synthesises a spurious Backspace on the input after a chip
133
+ // deletion moves DOM focus back to it. This flag suppresses that one event.
134
+ const ignoreNextBackspaceRef = React.useRef(false);
130
135
  const windowLostFocus = React.useRef(false);
131
136
  const [anchorEl, setAnchorEl] = React.useState(null);
132
137
  const [focusedItem, setFocusedItem] = React.useState(-1);
133
138
  const defaultHighlighted = autoHighlight ? 0 : -1;
134
139
  const highlightedIndexRef = React.useRef(defaultHighlighted);
135
140
 
141
+ // Tracks how the current highlight was set:
142
+ // - 'keyboard' — arrow keys, Home/End, PageUp/PageDown
143
+ // - 'mouse' — handleOptionMouseMove
144
+ // - 'touch' — handleOptionTouchStart
145
+ // - null — programmatic (autoHighlight, value sync)
146
+ //
147
+ // This lets handleBlur and the Enter handler distinguish intentional
148
+ // interactions from incidental ones — e.g. autoSelect should not commit
149
+ // a highlight that came from a casual mouse hover.
150
+ /** @type {React.RefObject<AutocompleteHighlightChangeReason | null>} */
151
+ const highlightReasonRef = React.useRef(null);
152
+ const touchScrolledRef = React.useRef(false);
153
+ const isTouchRef = React.useRef(false);
154
+
136
155
  // Calculate the initial inputValue on mount only.
137
156
  // useRef ensures it doesn't update dynamically with defaultValue or value props.
138
157
  const initialInputValue = React.useRef(getInputValue(defaultValue ?? valueProp, multiple, getOptionLabel)).current;
@@ -149,10 +168,12 @@ function useAutocomplete(props) {
149
168
  });
150
169
  const [focused, setFocused] = React.useState(false);
151
170
  const resetInputValue = React.useCallback((event, newValue, reason) => {
152
- // retain current `inputValue` if new option isn't selected and `clearOnBlur` is false
153
- // When `multiple` is enabled, `newValue` is an array of all selected items including the newly selected item
171
+ // Retain the current `inputValue` when no new option is selected and `clearOnBlur` is false.
172
+ // In `multiple` mode, `newValue` is the next value array, so only length growth counts as a selection.
154
173
  const isOptionSelected = multiple ? value.length < newValue.length : newValue !== null;
155
- if (!isOptionSelected && !clearOnBlur) {
174
+ // A controlled single-value `freeSolo` reset to `null` should still clear the input.
175
+ const shouldClearOnReset = reason === 'reset' && freeSolo && !multiple && newValue === null;
176
+ if (!isOptionSelected && !clearOnBlur && !shouldClearOnReset) {
156
177
  return;
157
178
  }
158
179
  const newInputValue = getInputValue(newValue, multiple, getOptionLabel, renderValue);
@@ -163,7 +184,7 @@ function useAutocomplete(props) {
163
184
  if (onInputChange) {
164
185
  onInputChange(event, newInputValue, reason);
165
186
  }
166
- }, [getOptionLabel, inputValue, multiple, onInputChange, setInputValueState, clearOnBlur, value, renderValue]);
187
+ }, [getOptionLabel, inputValue, multiple, onInputChange, setInputValueState, clearOnBlur, freeSolo, value, renderValue]);
167
188
  const [open, setOpenState] = useControlled({
168
189
  controlled: openProp,
169
190
  default: false,
@@ -218,8 +239,10 @@ function useAutocomplete(props) {
218
239
  return;
219
240
  }
220
241
 
221
- // Only reset the input's value when freeSolo if the component's value changes.
222
- if (freeSolo && !valueChange) {
242
+ // In freeSolo mode, only reset the input after a real value change.
243
+ // Also prevent the initial default value of `null` from clearing controlled values.
244
+ const shouldSkipFreeSoloReset = freeSolo && (!valueChange || value == null && previousProps.value === undefined);
245
+ if (shouldSkipFreeSoloReset) {
223
246
  return;
224
247
  }
225
248
  resetInputValue(null, value, 'reset');
@@ -270,12 +293,16 @@ function useAutocomplete(props) {
270
293
  }
271
294
  }
272
295
  }
273
- const setHighlightedIndex = useEventCallback(({
274
- event,
296
+ const syncHighlightedIndexToDOM = useEventCallback(({
275
297
  index,
276
- reason
298
+ reason,
299
+ preserveScroll = false
277
300
  }) => {
278
- highlightedIndexRef.current = index;
301
+ // React can clear refs before pending passive effects run during unmount.
302
+ // If both refs are gone, there is no DOM left to sync.
303
+ if (inputRef.current == null && listboxRef.current == null) {
304
+ return;
305
+ }
279
306
 
280
307
  // does the index exist?
281
308
  if (index === -1) {
@@ -283,9 +310,6 @@ function useAutocomplete(props) {
283
310
  } else {
284
311
  inputRef.current.setAttribute('aria-activedescendant', `${id}-option-${index}`);
285
312
  }
286
- if (onHighlightChange && ['mouse', 'keyboard', 'touch'].includes(reason)) {
287
- onHighlightChange(event, index === -1 ? null : filteredOptions[index], reason);
288
- }
289
313
  if (!listboxRef.current) {
290
314
  return;
291
315
  }
@@ -304,7 +328,9 @@ function useAutocomplete(props) {
304
328
  return;
305
329
  }
306
330
  if (index === -1) {
307
- listboxNode.scrollTop = 0;
331
+ if (!preserveScroll) {
332
+ listboxNode.scrollTop = 0;
333
+ }
308
334
  return;
309
335
  }
310
336
  const option = listboxRef.current.querySelector(`[data-option-index="${index}"]`);
@@ -332,15 +358,46 @@ function useAutocomplete(props) {
332
358
  }
333
359
  }
334
360
  });
361
+ const setHighlightedIndex = useEventCallback(({
362
+ event,
363
+ index,
364
+ reason,
365
+ preserveScroll = false
366
+ }) => {
367
+ highlightedIndexRef.current = index;
368
+ highlightReasonRef.current = reason ?? null;
369
+ if (onHighlightChange && ['mouse', 'keyboard', 'touch'].includes(reason)) {
370
+ onHighlightChange(event, index === -1 ? null : filteredOptions[index], reason);
371
+ }
372
+ syncHighlightedIndexToDOM({
373
+ index,
374
+ reason,
375
+ preserveScroll
376
+ });
377
+ });
378
+ const setHighlightedIndexFromSync = useEventCallback(({
379
+ index
380
+ }) => {
381
+ highlightedIndexRef.current = index;
382
+ syncHighlightedIndexToDOM({
383
+ index,
384
+ reason: highlightReasonRef.current
385
+ });
386
+ });
335
387
  const changeHighlightedIndex = useEventCallback(({
336
388
  event,
337
389
  diff,
338
390
  direction = 'next',
339
- reason
391
+ reason,
392
+ preserveScroll
340
393
  }) => {
341
394
  if (!popupOpen) {
342
395
  return;
343
396
  }
397
+ if (reason === 'keyboard') {
398
+ touchScrolledRef.current = false;
399
+ isTouchRef.current = false;
400
+ }
344
401
  const getNextIndex = () => {
345
402
  const maxIndex = filteredOptions.length - 1;
346
403
  if (diff === 'reset') {
@@ -377,7 +434,8 @@ function useAutocomplete(props) {
377
434
  setHighlightedIndex({
378
435
  index: nextIndex,
379
436
  reason,
380
- event
437
+ event,
438
+ preserveScroll
381
439
  });
382
440
 
383
441
  // Sync the content of the input with the highlighted option.
@@ -431,15 +489,24 @@ function useAutocomplete(props) {
431
489
  // If it exists and the value and the inputValue haven't changed, just update its index, otherwise continue execution
432
490
  const previousHighlightedOptionIndex = getPreviousHighlightedOptionIndex();
433
491
  if (previousHighlightedOptionIndex !== -1) {
434
- highlightedIndexRef.current = previousHighlightedOptionIndex;
492
+ // Keep the original highlight reason while re-syncing the DOM state.
493
+ // The highlighted option still exists after the filteredOptions array changed
494
+ // (e.g. async fetch returns new options while the user is mid-navigation),
495
+ // so the original interaction reason (keyboard, mouse, etc.) still applies.
496
+ setHighlightedIndexFromSync({
497
+ index: previousHighlightedOptionIndex
498
+ });
435
499
  return;
436
500
  }
437
501
  const valueItem = multiple ? value[0] : value;
438
502
 
439
503
  // The popup is empty, reset
440
504
  if (filteredOptions.length === 0 || valueItem == null) {
505
+ // Preserve scroll when new options are appended without changing the current filter.
506
+ const isAppendOnly = filteredOptionsChanged && previousProps.inputValue === inputValue && previousProps.filteredOptions?.length > 0 && filteredOptions.length > previousProps.filteredOptions.length && previousProps.filteredOptions.every((option, index) => getOptionLabel(option) === getOptionLabel(filteredOptions[index]));
441
507
  changeHighlightedIndex({
442
- diff: 'reset'
508
+ diff: 'reset',
509
+ preserveScroll: isAppendOnly
443
510
  });
444
511
  return;
445
512
  }
@@ -451,8 +518,12 @@ function useAutocomplete(props) {
451
518
  if (valueItem != null) {
452
519
  const currentOption = filteredOptions[highlightedIndexRef.current];
453
520
 
454
- // Keep the current highlighted index if possible
455
- if (multiple && currentOption && value.findIndex(val => isOptionEqualToValue(currentOption, val)) !== -1) {
521
+ // Keep the current selected highlight while the popup stays open;
522
+ // on reopen, resync from the selected value.
523
+ if (multiple && currentOption && value.findIndex(val => isOptionEqualToValue(currentOption, val)) !== -1 && previousProps.filteredOptions?.length > 0) {
524
+ setHighlightedIndexFromSync({
525
+ index: highlightedIndexRef.current
526
+ });
456
527
  return;
457
528
  }
458
529
  const itemIndex = filteredOptions.findIndex(optionItem => isOptionEqualToValue(optionItem, valueItem));
@@ -487,7 +558,7 @@ function useAutocomplete(props) {
487
558
  filteredOptions.length,
488
559
  // Don't sync the highlighted index with the value when multiple
489
560
  // eslint-disable-next-line react-hooks/exhaustive-deps
490
- multiple ? false : value, changeHighlightedIndex, setHighlightedIndex, popupOpen, inputValue, multiple]);
561
+ multiple ? false : value, changeHighlightedIndex, setHighlightedIndex, setHighlightedIndexFromSync, popupOpen, inputValue, multiple]);
491
562
  const handleListboxRef = useEventCallback(node => {
492
563
  setRef(listboxRef, node);
493
564
  if (!node) {
@@ -533,6 +604,7 @@ function useAutocomplete(props) {
533
604
  }
534
605
  setOpenState(true);
535
606
  setInputPristine(true);
607
+ isTouchRef.current = false;
536
608
  if (onOpen) {
537
609
  onOpen(event);
538
610
  }
@@ -542,6 +614,8 @@ function useAutocomplete(props) {
542
614
  return;
543
615
  }
544
616
  setOpenState(false);
617
+ touchScrolledRef.current = false;
618
+ highlightReasonRef.current = null;
545
619
  if (onClose) {
546
620
  onClose(event, reason);
547
621
  }
@@ -559,7 +633,6 @@ function useAutocomplete(props) {
559
633
  }
560
634
  setValueState(newValue);
561
635
  };
562
- const isTouch = React.useRef(false);
563
636
  const selectNewValue = (event, option, reasonProp = 'selectOption', origin = 'options') => {
564
637
  let reason = reasonProp;
565
638
  let newValue = option;
@@ -586,7 +659,7 @@ function useAutocomplete(props) {
586
659
  if (!disableCloseOnSelect && (!event || !event.ctrlKey && !event.metaKey)) {
587
660
  handleClose(event, reason);
588
661
  }
589
- if (blurOnSelect === true || blurOnSelect === 'touch' && isTouch.current || blurOnSelect === 'mouse' && !isTouch.current) {
662
+ if (blurOnSelect === true || blurOnSelect === 'touch' && isTouchRef.current || blurOnSelect === 'mouse' && !isTouchRef.current) {
590
663
  inputRef.current.blur();
591
664
  }
592
665
  };
@@ -644,7 +717,6 @@ function useAutocomplete(props) {
644
717
  focusItem(nextItem);
645
718
  };
646
719
  const handleClear = event => {
647
- ignoreFocus.current = true;
648
720
  setInputValueState('');
649
721
  if (onInputChange) {
650
722
  onInputChange(event, '', 'clear');
@@ -772,29 +844,47 @@ function useAutocomplete(props) {
772
844
  }
773
845
  break;
774
846
  case 'Enter':
775
- if (highlightedIndexRef.current !== -1 && popupOpen) {
776
- const option = filteredOptions[highlightedIndexRef.current];
777
- const disabled = getOptionDisabled ? getOptionDisabled(option) : false;
847
+ {
848
+ // In freeSolo, only select the highlighted option if the user hasn't
849
+ // typed new text (inputPristine) or explicitly interacted with an option
850
+ // (keyboard, mouse, or touch — any non-null reason). This lets typed
851
+ // text win over a programmatic highlight (reason=null, e.g. from
852
+ // syncHighlightedIndex matching a previous value) while still honoring
853
+ // deliberate user interactions like hovering a suggestion then pressing Enter.
854
+ const shouldSelectHighlighted = !freeSolo || inputPristine || highlightReasonRef.current !== null;
855
+ if (highlightedIndexRef.current !== -1 && popupOpen && shouldSelectHighlighted &&
856
+ // After a touch-scroll the highlight is stale (the user scrolled
857
+ // past it), so skip selection until the next deliberate interaction.
858
+ !touchScrolledRef.current) {
859
+ const option = filteredOptions[highlightedIndexRef.current];
860
+ const disabled = getOptionDisabled ? getOptionDisabled(option) : false;
778
861
 
779
- // Avoid early form validation, let the end-users continue filling the form.
780
- event.preventDefault();
781
- if (disabled) {
782
- return;
783
- }
784
- selectNewValue(event, option, 'selectOption');
862
+ // Avoid early form validation, let the end-users continue filling the form.
863
+ event.preventDefault();
864
+ if (disabled) {
865
+ return;
866
+ }
867
+ selectNewValue(event, option, 'selectOption');
785
868
 
786
- // Move the selection to the end.
787
- if (autoComplete) {
788
- inputRef.current.setSelectionRange(inputRef.current.value.length, inputRef.current.value.length);
789
- }
790
- } else if (freeSolo && inputValue !== '' && inputValueIsSelectedValue === false) {
791
- if (multiple) {
792
- // Allow people to add new values before they submit the form.
869
+ // Move the selection to the end.
870
+ if (autoComplete) {
871
+ inputRef.current.setSelectionRange(inputRef.current.value.length, inputRef.current.value.length);
872
+ }
873
+ } else if (freeSolo && inputValue !== '' && inputValueIsSelectedValue === false) {
874
+ if (multiple) {
875
+ // Allow people to add new values before they submit the form.
876
+ event.preventDefault();
877
+ }
878
+ selectNewValue(event, inputValue, 'createOption', 'freeSolo');
879
+ } else if (popupOpen && touchScrolledRef.current) {
880
+ // The highlight is stale from a touch-scroll - close without selecting.
793
881
  event.preventDefault();
882
+ // This happens on Enter, but re-using "escape" as the closest `AutocompleteCloseReason`
883
+ // to avoid creating a new reason
884
+ handleClose(event, 'escape');
794
885
  }
795
- selectNewValue(event, inputValue, 'createOption', 'freeSolo');
886
+ break;
796
887
  }
797
- break;
798
888
  case 'Escape':
799
889
  if (popupOpen) {
800
890
  // Avoid Opera to exit fullscreen mode.
@@ -812,6 +902,10 @@ function useAutocomplete(props) {
812
902
  break;
813
903
  case 'Backspace':
814
904
  // Remove the value on the left of the "cursor"
905
+ if (ignoreNextBackspaceRef.current) {
906
+ ignoreNextBackspaceRef.current = false;
907
+ break;
908
+ }
815
909
  if (multiple && !readOnly && inputValue === '' && value.length > 0) {
816
910
  const index = focusedItem === -1 ? value.length - 1 : focusedItem;
817
911
  const newValue = value.slice();
@@ -819,6 +913,18 @@ function useAutocomplete(props) {
819
913
  handleValue(event, newValue, 'removeOption', {
820
914
  option: value[index]
821
915
  });
916
+ if (focusedItem !== -1) {
917
+ // Suppress the spurious Backspace VoiceOver synthesises on the
918
+ // input after focus returns to it. Clear it shortly after
919
+ // deletion so a later real Backspace is not ignored if the
920
+ // synthetic follow-up event never arrives.
921
+ ignoreNextBackspaceRef.current = true;
922
+ setTimeout(() => {
923
+ if (ignoreNextBackspaceRef.current) {
924
+ ignoreNextBackspaceRef.current = false;
925
+ }
926
+ }, 0);
927
+ }
822
928
  }
823
929
  if (!multiple && renderValue && !readOnly && inputValue === '') {
824
930
  handleValue(event, null, 'removeOption', {
@@ -872,7 +978,7 @@ function useAutocomplete(props) {
872
978
  }
873
979
  };
874
980
  const handleBlur = event => {
875
- // Ignore the event when using the scrollbar with IE11
981
+ // Ignore the event when using the scrollbar with IE 11
876
982
  if (unstable_isActiveElementInListbox(listboxRef)) {
877
983
  inputRef.current.focus();
878
984
  return;
@@ -880,7 +986,12 @@ function useAutocomplete(props) {
880
986
  setFocused(false);
881
987
  firstFocus.current = true;
882
988
  ignoreFocus.current = false;
883
- if (autoSelect && highlightedIndexRef.current !== -1 && popupOpen) {
989
+
990
+ // Auto-select the highlighted option on blur, but only if the highlight
991
+ // came from keyboard navigation or was set programmatically (autoHighlight).
992
+ // Mouse hover and touch should not trigger selection — the user may have
993
+ // moved the pointer over an option without intending to commit to it.
994
+ if (autoSelect && highlightedIndexRef.current !== -1 && popupOpen && highlightReasonRef.current !== 'mouse' && highlightReasonRef.current !== 'touch') {
884
995
  selectNewValue(event, filteredOptions[highlightedIndexRef.current], 'blur');
885
996
  } else if (autoSelect && freeSolo && inputValue !== '') {
886
997
  selectNewValue(event, inputValue, 'blur', 'freeSolo');
@@ -891,9 +1002,10 @@ function useAutocomplete(props) {
891
1002
  };
892
1003
  const handleInputChange = event => {
893
1004
  const newValue = event.target.value;
894
- if (inputValue !== newValue) {
1005
+ const valueChanged = inputValue !== newValue;
1006
+ if (valueChanged) {
895
1007
  setInputValueState(newValue);
896
- setInputPristine(false);
1008
+ touchScrolledRef.current = false;
897
1009
  if (onInputChange) {
898
1010
  onInputChange(event, newValue, 'input');
899
1011
  }
@@ -907,6 +1019,12 @@ function useAutocomplete(props) {
907
1019
  } else {
908
1020
  handleOpen(event);
909
1021
  }
1022
+
1023
+ // Called after handleOpen so it overrides handleOpen's setInputPristine(true)
1024
+ // when the first keystroke also opens the popup.
1025
+ if (valueChanged) {
1026
+ setInputPristine(false);
1027
+ }
910
1028
  };
911
1029
  const handleOptionMouseMove = event => {
912
1030
  const index = Number(event.currentTarget.getAttribute('data-option-index'));
@@ -916,20 +1034,46 @@ function useAutocomplete(props) {
916
1034
  index,
917
1035
  reason: 'mouse'
918
1036
  });
1037
+ } else {
1038
+ // The option is already highlighted (e.g. programmatically via autoHighlight),
1039
+ // but the user moved the mouse over it — mark as mouse-initiated so
1040
+ // autoSelect on blur correctly treats this as incidental hover.
1041
+ highlightReasonRef.current = 'mouse';
1042
+ }
1043
+ // Don't clear the touch-scroll guard while touch state is still latched.
1044
+ // After a touch gesture, browsers may fire compatibility mousemove
1045
+ // events; if those cleared the guard immediately, later compat events in
1046
+ // the same sequence could be misclassified as a real mouse interaction.
1047
+ // Touch state is cleared by the next deliberate interaction
1048
+ // (keyboard nav, handleOptionClick, or handleOpen).
1049
+ if (!isTouchRef.current) {
1050
+ touchScrolledRef.current = false;
1051
+ }
1052
+ };
1053
+ const handleListboxMouseLeave = event => {
1054
+ if (!resetHighlightOnMouseLeave || highlightedIndexRef.current === -1 || highlightReasonRef.current !== 'mouse' || isTouchRef.current) {
1055
+ return;
919
1056
  }
1057
+ setHighlightedIndex({
1058
+ event,
1059
+ index: -1,
1060
+ reason: 'mouse',
1061
+ preserveScroll: true
1062
+ });
920
1063
  };
921
1064
  const handleOptionTouchStart = event => {
1065
+ touchScrolledRef.current = false;
922
1066
  setHighlightedIndex({
923
1067
  event,
924
1068
  index: Number(event.currentTarget.getAttribute('data-option-index')),
925
1069
  reason: 'touch'
926
1070
  });
927
- isTouch.current = true;
1071
+ isTouchRef.current = true;
928
1072
  };
929
1073
  const handleOptionClick = event => {
930
1074
  const index = Number(event.currentTarget.getAttribute('data-option-index'));
931
1075
  selectNewValue(event, filteredOptions[index], 'selectOption');
932
- isTouch.current = false;
1076
+ isTouchRef.current = false;
933
1077
  };
934
1078
  const handleItemDelete = index => event => {
935
1079
  const newValue = value.slice();
@@ -954,11 +1098,11 @@ function useAutocomplete(props) {
954
1098
  // Prevent input blur when interacting with the combobox
955
1099
  const handleMouseDown = event => {
956
1100
  // Prevent focusing the input if click is anywhere outside the Autocomplete
957
- if (!event.currentTarget.contains(event.target)) {
1101
+ if (!contains(event.currentTarget, event.target)) {
958
1102
  return;
959
1103
  }
960
1104
  // Don't interfere with interactions outside the input area (e.g. helper text)
961
- if (anchorEl && !anchorEl.contains(event.target)) {
1105
+ if (anchorEl && !contains(anchorEl, event.target)) {
962
1106
  return;
963
1107
  }
964
1108
  if (event.target.getAttribute('id') !== id) {
@@ -969,11 +1113,11 @@ function useAutocomplete(props) {
969
1113
  // Focus the input when interacting with the combobox
970
1114
  const handleClick = event => {
971
1115
  // Prevent focusing the input if click is anywhere outside the Autocomplete
972
- if (!event.currentTarget.contains(event.target)) {
1116
+ if (!contains(event.currentTarget, event.target)) {
973
1117
  return;
974
1118
  }
975
1119
  // Don't interfere with interactions outside the input area (e.g. helper text)
976
- if (anchorEl && !anchorEl.contains(event.target)) {
1120
+ if (anchorEl && !contains(anchorEl, event.target)) {
977
1121
  return;
978
1122
  }
979
1123
  inputRef.current.focus();
@@ -1057,7 +1201,10 @@ function useAutocomplete(props) {
1057
1201
  getClearProps: () => ({
1058
1202
  tabIndex: -1,
1059
1203
  type: 'button',
1060
- onClick: handleClear
1204
+ onClick: event => {
1205
+ ignoreFocus.current = true;
1206
+ handleClear(event);
1207
+ }
1061
1208
  }),
1062
1209
  getItemProps: ({
1063
1210
  index = 0
@@ -1067,6 +1214,12 @@ function useAutocomplete(props) {
1067
1214
  }),
1068
1215
  'data-item-index': index,
1069
1216
  tabIndex: -1,
1217
+ onFocus: () => {
1218
+ // If focus on the item doesn't come from keyboard events, we update the state via onFocus.
1219
+ if (focusedItem !== index) {
1220
+ setFocusedItem(index);
1221
+ }
1222
+ },
1070
1223
  ...(!readOnly && {
1071
1224
  onDelete: multiple ? handleItemDelete(index) : handleSingleItemDelete
1072
1225
  })
@@ -1076,15 +1229,37 @@ function useAutocomplete(props) {
1076
1229
  type: 'button',
1077
1230
  onClick: handlePopupIndicator
1078
1231
  }),
1079
- getListboxProps: () => ({
1232
+ getListboxProps: (other = {}) => ({
1233
+ ...other,
1080
1234
  role: 'listbox',
1081
1235
  id: `${id}-listbox`,
1082
1236
  'aria-labelledby': `${id}-label`,
1083
1237
  'aria-multiselectable': multiple || undefined,
1084
1238
  ref: handleListboxRef,
1085
1239
  onMouseDown: event => {
1240
+ other.onMouseDown?.(event);
1241
+ if (event.defaultMuiPrevented) {
1242
+ return;
1243
+ }
1244
+
1086
1245
  // Prevent blur
1087
1246
  event.preventDefault();
1247
+ },
1248
+ onScroll: event => {
1249
+ other.onScroll?.(event);
1250
+ if (event.defaultMuiPrevented) {
1251
+ return;
1252
+ }
1253
+ if (isTouchRef.current) {
1254
+ touchScrolledRef.current = true;
1255
+ }
1256
+ },
1257
+ onMouseLeave: event => {
1258
+ other.onMouseLeave?.(event);
1259
+ if (event.defaultMuiPrevented) {
1260
+ return;
1261
+ }
1262
+ handleListboxMouseLeave(event);
1088
1263
  }
1089
1264
  }),
1090
1265
  getOptionProps: ({
@@ -0,0 +1,2 @@
1
+ import contains from '@mui/utils/contains';
2
+ export default contains;
@@ -0,0 +1,2 @@
1
+ import contains from '@mui/utils/contains';
2
+ export default contains;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.default = void 0;
8
+ var _contains = _interopRequireDefault(require("@mui/utils/contains"));
9
+ var _default = exports.default = _contains.default;
@@ -0,0 +1,2 @@
1
+ import contains from '@mui/utils/contains';
2
+ export default contains;
@@ -0,0 +1,7 @@
1
+ export declare const FOCUSABLE_ATTRIBUTE = "data-mui-focusable";
2
+ /**
3
+ * Returns the element marked as the initial focus target inside a focus trap.
4
+ * The root element takes precedence over marked descendants so components can
5
+ * opt into focusing their own root surface directly.
6
+ */
7
+ export declare function getFocusTarget(rootElement: HTMLElement | null | undefined): HTMLElement | null;
@@ -0,0 +1,7 @@
1
+ export declare const FOCUSABLE_ATTRIBUTE = "data-mui-focusable";
2
+ /**
3
+ * Returns the element marked as the initial focus target inside a focus trap.
4
+ * The root element takes precedence over marked descendants so components can
5
+ * opt into focusing their own root surface directly.
6
+ */
7
+ export declare function getFocusTarget(rootElement: HTMLElement | null | undefined): HTMLElement | null;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.FOCUSABLE_ATTRIBUTE = void 0;
7
+ exports.getFocusTarget = getFocusTarget;
8
+ const FOCUSABLE_ATTRIBUTE = exports.FOCUSABLE_ATTRIBUTE = 'data-mui-focusable';
9
+
10
+ /**
11
+ * Returns the element marked as the initial focus target inside a focus trap.
12
+ * The root element takes precedence over marked descendants so components can
13
+ * opt into focusing their own root surface directly.
14
+ */
15
+ function getFocusTarget(rootElement) {
16
+ if (!rootElement) {
17
+ return null;
18
+ }
19
+ return rootElement.hasAttribute(FOCUSABLE_ATTRIBUTE) ? rootElement : rootElement.querySelector(`[${FOCUSABLE_ATTRIBUTE}]`);
20
+ }
@@ -0,0 +1,13 @@
1
+ export const FOCUSABLE_ATTRIBUTE = 'data-mui-focusable';
2
+
3
+ /**
4
+ * Returns the element marked as the initial focus target inside a focus trap.
5
+ * The root element takes precedence over marked descendants so components can
6
+ * opt into focusing their own root surface directly.
7
+ */
8
+ export function getFocusTarget(rootElement) {
9
+ if (!rootElement) {
10
+ return null;
11
+ }
12
+ return rootElement.hasAttribute(FOCUSABLE_ATTRIBUTE) ? rootElement : rootElement.querySelector(`[${FOCUSABLE_ATTRIBUTE}]`);
13
+ }
@@ -0,0 +1,2 @@
1
+ import getEventTarget from '@mui/utils/getEventTarget';
2
+ export default getEventTarget;
@@ -0,0 +1,2 @@
1
+ import getEventTarget from '@mui/utils/getEventTarget';
2
+ export default getEventTarget;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.default = void 0;
8
+ var _getEventTarget = _interopRequireDefault(require("@mui/utils/getEventTarget"));
9
+ var _default = exports.default = _getEventTarget.default;
@@ -0,0 +1,2 @@
1
+ import getEventTarget from '@mui/utils/getEventTarget';
2
+ export default getEventTarget;