@mui/material 9.0.0 → 9.0.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 (130) hide show
  1. package/Autocomplete/Autocomplete.js +65 -11
  2. package/Autocomplete/Autocomplete.mjs +65 -11
  3. package/Avatar/Avatar.js +4 -0
  4. package/Avatar/Avatar.mjs +4 -0
  5. package/Badge/Badge.js +3 -0
  6. package/Badge/Badge.mjs +3 -0
  7. package/Button/Button.js +19 -2
  8. package/Button/Button.mjs +19 -2
  9. package/ButtonBase/ButtonBase.d.mts +7 -0
  10. package/ButtonBase/ButtonBase.d.ts +7 -0
  11. package/ButtonBase/ButtonBase.js +5 -2
  12. package/ButtonBase/ButtonBase.mjs +5 -2
  13. package/CHANGELOG.md +132 -1245
  14. package/Checkbox/Checkbox.js +2 -1
  15. package/Checkbox/Checkbox.mjs +2 -1
  16. package/CircularProgress/CircularProgress.d.mts +12 -2
  17. package/CircularProgress/CircularProgress.d.ts +12 -2
  18. package/CircularProgress/CircularProgress.js +33 -6
  19. package/CircularProgress/CircularProgress.mjs +33 -6
  20. package/ClickAwayListener/ClickAwayListener.js +3 -6
  21. package/ClickAwayListener/ClickAwayListener.mjs +3 -6
  22. package/Dialog/Dialog.js +11 -6
  23. package/Dialog/Dialog.mjs +11 -6
  24. package/Drawer/Drawer.js +18 -4
  25. package/Drawer/Drawer.mjs +18 -4
  26. package/Fab/Fab.js +7 -1
  27. package/Fab/Fab.mjs +7 -1
  28. package/FilledInput/FilledInput.d.mts +4 -0
  29. package/FilledInput/FilledInput.d.ts +4 -0
  30. package/FilledInput/FilledInput.js +18 -20
  31. package/FilledInput/FilledInput.mjs +18 -20
  32. package/FormControl/useFormControl.d.mts +12 -2
  33. package/FormControl/useFormControl.d.ts +12 -2
  34. package/FormControl/useFormControl.js +13 -0
  35. package/FormControl/useFormControl.mjs +12 -0
  36. package/FormControlLabel/FormControlLabel.js +5 -8
  37. package/FormControlLabel/FormControlLabel.mjs +5 -8
  38. package/FormGroup/FormGroup.js +2 -5
  39. package/FormGroup/FormGroup.mjs +2 -5
  40. package/FormHelperText/FormHelperText.js +2 -5
  41. package/FormHelperText/FormHelperText.mjs +2 -5
  42. package/FormLabel/FormLabel.js +2 -5
  43. package/FormLabel/FormLabel.mjs +2 -5
  44. package/IconButton/IconButton.js +1 -8
  45. package/IconButton/IconButton.mjs +1 -8
  46. package/Input/Input.d.mts +4 -0
  47. package/Input/Input.d.ts +4 -0
  48. package/Input/Input.js +6 -0
  49. package/Input/Input.mjs +6 -0
  50. package/InputBase/InputBase.d.mts +2 -1
  51. package/InputBase/InputBase.d.ts +2 -1
  52. package/InputBase/InputBase.js +50 -15
  53. package/InputBase/InputBase.mjs +50 -15
  54. package/InputLabel/InputLabel.js +5 -8
  55. package/InputLabel/InputLabel.mjs +5 -8
  56. package/LinearProgress/LinearProgress.d.mts +12 -2
  57. package/LinearProgress/LinearProgress.d.ts +12 -2
  58. package/LinearProgress/LinearProgress.js +42 -10
  59. package/LinearProgress/LinearProgress.mjs +42 -10
  60. package/List/List.js +2 -1
  61. package/List/List.mjs +2 -1
  62. package/ListItemButton/ListItemButton.js +7 -1
  63. package/ListItemButton/ListItemButton.mjs +7 -1
  64. package/MenuItem/MenuItem.js +7 -1
  65. package/MenuItem/MenuItem.mjs +7 -1
  66. package/MenuList/MenuList.js +2 -1
  67. package/MenuList/MenuList.mjs +2 -1
  68. package/NativeSelect/NativeSelect.js +2 -5
  69. package/NativeSelect/NativeSelect.mjs +2 -5
  70. package/OutlinedInput/OutlinedInput.js +13 -23
  71. package/OutlinedInput/OutlinedInput.mjs +13 -23
  72. package/PigmentContainer/PigmentContainer.js +0 -1
  73. package/PigmentContainer/PigmentContainer.mjs +0 -1
  74. package/Popper/BasePopper.js +23 -1
  75. package/Popper/BasePopper.mjs +23 -1
  76. package/Select/Select.js +2 -5
  77. package/Select/Select.mjs +2 -5
  78. package/Select/SelectInput.js +164 -2
  79. package/Select/SelectInput.mjs +164 -2
  80. package/Slide/Slide.js +48 -26
  81. package/Slide/Slide.mjs +49 -27
  82. package/Slider/Slider.js +10 -1
  83. package/Slider/Slider.mjs +10 -1
  84. package/Slider/useSlider.js +3 -2
  85. package/Slider/useSlider.mjs +3 -2
  86. package/SwipeableDrawer/SwipeableDrawer.js +7 -3
  87. package/SwipeableDrawer/SwipeableDrawer.mjs +7 -3
  88. package/Switch/Switch.js +7 -6
  89. package/Switch/Switch.mjs +7 -6
  90. package/Tabs/ScrollbarSize.js +2 -1
  91. package/Tabs/ScrollbarSize.mjs +2 -1
  92. package/Tabs/Tabs.js +2 -1
  93. package/Tabs/Tabs.mjs +2 -1
  94. package/Tooltip/Tooltip.js +26 -108
  95. package/Tooltip/Tooltip.mjs +26 -108
  96. package/Unstable_TrapFocus/FocusTrap.js +18 -14
  97. package/Unstable_TrapFocus/FocusTrap.mjs +18 -14
  98. package/index.js +1 -1
  99. package/index.mjs +1 -1
  100. package/package.json +49 -49
  101. package/styles/responsiveFontSizes.js +19 -8
  102. package/styles/responsiveFontSizes.mjs +19 -8
  103. package/styles/useThemeProps.d.mts +3 -3
  104. package/styles/useThemeProps.d.ts +3 -3
  105. package/transitions/utils.d.mts +17 -0
  106. package/transitions/utils.d.ts +17 -0
  107. package/transitions/utils.js +64 -0
  108. package/transitions/utils.mjs +63 -0
  109. package/useAutocomplete/useAutocomplete.d.mts +4 -5
  110. package/useAutocomplete/useAutocomplete.d.ts +4 -5
  111. package/useAutocomplete/useAutocomplete.js +166 -53
  112. package/useAutocomplete/useAutocomplete.mjs +166 -53
  113. package/utils/contains.d.mts +2 -0
  114. package/utils/contains.d.ts +2 -0
  115. package/utils/contains.js +9 -0
  116. package/utils/contains.mjs +2 -0
  117. package/utils/focusable.d.mts +7 -0
  118. package/utils/focusable.d.ts +7 -0
  119. package/utils/focusable.js +20 -0
  120. package/utils/focusable.mjs +13 -0
  121. package/utils/getEventTarget.d.mts +2 -0
  122. package/utils/getEventTarget.d.ts +2 -0
  123. package/utils/getEventTarget.js +9 -0
  124. package/utils/getEventTarget.mjs +2 -0
  125. package/utils/mergeSlotProps.js +2 -8
  126. package/utils/mergeSlotProps.mjs +1 -8
  127. package/version/index.js +2 -2
  128. package/version/index.mjs +2 -2
  129. package/FormControl/formControlState.js +0 -21
  130. package/FormControl/formControlState.mjs +0 -15
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", {
9
9
  exports.createFilterOptions = createFilterOptions;
10
10
  exports.default = void 0;
11
11
  var React = _interopRequireWildcard(require("react"));
12
+ var _contains = _interopRequireDefault(require("@mui/utils/contains"));
12
13
  var _setRef = _interopRequireDefault(require("@mui/utils/setRef"));
13
14
  var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
14
15
  var _useControlled = _interopRequireDefault(require("@mui/utils/useControlled"));
@@ -63,7 +64,7 @@ const defaultFilterOptions = createFilterOptions();
63
64
 
64
65
  // Number of options to jump in list box when `Page Up` and `Page Down` keys are used.
65
66
  const pageSize = 5;
66
- const defaultIsActiveElementInListbox = listboxRef => listboxRef.current !== null && listboxRef.current.parentElement?.contains(document.activeElement);
67
+ const defaultIsActiveElementInListbox = listboxRef => listboxRef.current !== null && (0, _contains.default)(listboxRef.current.parentElement, document.activeElement);
67
68
  const defaultIsOptionEqualToValue = (option, value) => option === value;
68
69
  const MULTIPLE_DEFAULT_VALUE = [];
69
70
  function getInputValue(value, multiple, getOptionLabel, renderValue) {
@@ -141,6 +142,20 @@ function useAutocomplete(props) {
141
142
  const defaultHighlighted = autoHighlight ? 0 : -1;
142
143
  const highlightedIndexRef = React.useRef(defaultHighlighted);
143
144
 
145
+ // Tracks how the current highlight was set:
146
+ // - 'keyboard' — arrow keys, Home/End, PageUp/PageDown
147
+ // - 'mouse' — handleOptionMouseMove
148
+ // - 'touch' — handleOptionTouchStart
149
+ // - null — programmatic (autoHighlight, value sync)
150
+ //
151
+ // This lets handleBlur and the Enter handler distinguish intentional
152
+ // interactions from incidental ones — e.g. autoSelect should not commit
153
+ // a highlight that came from a casual mouse hover.
154
+ /** @type {React.RefObject<AutocompleteHighlightChangeReason | null>} */
155
+ const highlightReasonRef = React.useRef(null);
156
+ const touchScrolledRef = React.useRef(false);
157
+ const isTouchRef = React.useRef(false);
158
+
144
159
  // Calculate the initial inputValue on mount only.
145
160
  // useRef ensures it doesn't update dynamically with defaultValue or value props.
146
161
  const initialInputValue = React.useRef(getInputValue(defaultValue ?? valueProp, multiple, getOptionLabel)).current;
@@ -157,10 +172,12 @@ function useAutocomplete(props) {
157
172
  });
158
173
  const [focused, setFocused] = React.useState(false);
159
174
  const resetInputValue = React.useCallback((event, newValue, reason) => {
160
- // retain current `inputValue` if new option isn't selected and `clearOnBlur` is false
161
- // When `multiple` is enabled, `newValue` is an array of all selected items including the newly selected item
175
+ // Retain the current `inputValue` when no new option is selected and `clearOnBlur` is false.
176
+ // In `multiple` mode, `newValue` is the next value array, so only length growth counts as a selection.
162
177
  const isOptionSelected = multiple ? value.length < newValue.length : newValue !== null;
163
- if (!isOptionSelected && !clearOnBlur) {
178
+ // A controlled single-value `freeSolo` reset to `null` should still clear the input.
179
+ const shouldClearOnReset = reason === 'reset' && freeSolo && !multiple && newValue === null;
180
+ if (!isOptionSelected && !clearOnBlur && !shouldClearOnReset) {
164
181
  return;
165
182
  }
166
183
  const newInputValue = getInputValue(newValue, multiple, getOptionLabel, renderValue);
@@ -171,7 +188,7 @@ function useAutocomplete(props) {
171
188
  if (onInputChange) {
172
189
  onInputChange(event, newInputValue, reason);
173
190
  }
174
- }, [getOptionLabel, inputValue, multiple, onInputChange, setInputValueState, clearOnBlur, value, renderValue]);
191
+ }, [getOptionLabel, inputValue, multiple, onInputChange, setInputValueState, clearOnBlur, freeSolo, value, renderValue]);
175
192
  const [open, setOpenState] = (0, _useControlled.default)({
176
193
  controlled: openProp,
177
194
  default: false,
@@ -278,22 +295,17 @@ function useAutocomplete(props) {
278
295
  }
279
296
  }
280
297
  }
281
- const setHighlightedIndex = (0, _useEventCallback.default)(({
282
- event,
298
+ const syncHighlightedIndexToDOM = (0, _useEventCallback.default)(({
283
299
  index,
284
- reason
300
+ reason,
301
+ preserveScroll = false
285
302
  }) => {
286
- highlightedIndexRef.current = index;
287
-
288
303
  // does the index exist?
289
304
  if (index === -1) {
290
305
  inputRef.current.removeAttribute('aria-activedescendant');
291
306
  } else {
292
307
  inputRef.current.setAttribute('aria-activedescendant', `${id}-option-${index}`);
293
308
  }
294
- if (onHighlightChange && ['mouse', 'keyboard', 'touch'].includes(reason)) {
295
- onHighlightChange(event, index === -1 ? null : filteredOptions[index], reason);
296
- }
297
309
  if (!listboxRef.current) {
298
310
  return;
299
311
  }
@@ -312,7 +324,9 @@ function useAutocomplete(props) {
312
324
  return;
313
325
  }
314
326
  if (index === -1) {
315
- listboxNode.scrollTop = 0;
327
+ if (!preserveScroll) {
328
+ listboxNode.scrollTop = 0;
329
+ }
316
330
  return;
317
331
  }
318
332
  const option = listboxRef.current.querySelector(`[data-option-index="${index}"]`);
@@ -340,15 +354,46 @@ function useAutocomplete(props) {
340
354
  }
341
355
  }
342
356
  });
357
+ const setHighlightedIndex = (0, _useEventCallback.default)(({
358
+ event,
359
+ index,
360
+ reason,
361
+ preserveScroll = false
362
+ }) => {
363
+ highlightedIndexRef.current = index;
364
+ highlightReasonRef.current = reason ?? null;
365
+ if (onHighlightChange && ['mouse', 'keyboard', 'touch'].includes(reason)) {
366
+ onHighlightChange(event, index === -1 ? null : filteredOptions[index], reason);
367
+ }
368
+ syncHighlightedIndexToDOM({
369
+ index,
370
+ reason,
371
+ preserveScroll
372
+ });
373
+ });
374
+ const setHighlightedIndexFromSync = (0, _useEventCallback.default)(({
375
+ index
376
+ }) => {
377
+ highlightedIndexRef.current = index;
378
+ syncHighlightedIndexToDOM({
379
+ index,
380
+ reason: highlightReasonRef.current
381
+ });
382
+ });
343
383
  const changeHighlightedIndex = (0, _useEventCallback.default)(({
344
384
  event,
345
385
  diff,
346
386
  direction = 'next',
347
- reason
387
+ reason,
388
+ preserveScroll
348
389
  }) => {
349
390
  if (!popupOpen) {
350
391
  return;
351
392
  }
393
+ if (reason === 'keyboard') {
394
+ touchScrolledRef.current = false;
395
+ isTouchRef.current = false;
396
+ }
352
397
  const getNextIndex = () => {
353
398
  const maxIndex = filteredOptions.length - 1;
354
399
  if (diff === 'reset') {
@@ -385,7 +430,8 @@ function useAutocomplete(props) {
385
430
  setHighlightedIndex({
386
431
  index: nextIndex,
387
432
  reason,
388
- event
433
+ event,
434
+ preserveScroll
389
435
  });
390
436
 
391
437
  // Sync the content of the input with the highlighted option.
@@ -439,15 +485,24 @@ function useAutocomplete(props) {
439
485
  // If it exists and the value and the inputValue haven't changed, just update its index, otherwise continue execution
440
486
  const previousHighlightedOptionIndex = getPreviousHighlightedOptionIndex();
441
487
  if (previousHighlightedOptionIndex !== -1) {
442
- highlightedIndexRef.current = previousHighlightedOptionIndex;
488
+ // Keep the original highlight reason while re-syncing the DOM state.
489
+ // The highlighted option still exists after the filteredOptions array changed
490
+ // (e.g. async fetch returns new options while the user is mid-navigation),
491
+ // so the original interaction reason (keyboard, mouse, etc.) still applies.
492
+ setHighlightedIndexFromSync({
493
+ index: previousHighlightedOptionIndex
494
+ });
443
495
  return;
444
496
  }
445
497
  const valueItem = multiple ? value[0] : value;
446
498
 
447
499
  // The popup is empty, reset
448
500
  if (filteredOptions.length === 0 || valueItem == null) {
501
+ // Preserve scroll when new options are appended without changing the current filter.
502
+ 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]));
449
503
  changeHighlightedIndex({
450
- diff: 'reset'
504
+ diff: 'reset',
505
+ preserveScroll: isAppendOnly
451
506
  });
452
507
  return;
453
508
  }
@@ -459,8 +514,12 @@ function useAutocomplete(props) {
459
514
  if (valueItem != null) {
460
515
  const currentOption = filteredOptions[highlightedIndexRef.current];
461
516
 
462
- // Keep the current highlighted index if possible
463
- if (multiple && currentOption && value.findIndex(val => isOptionEqualToValue(currentOption, val)) !== -1) {
517
+ // Keep the current selected highlight while the popup stays open;
518
+ // on reopen, resync from the selected value.
519
+ if (multiple && currentOption && value.findIndex(val => isOptionEqualToValue(currentOption, val)) !== -1 && previousProps.filteredOptions?.length > 0) {
520
+ setHighlightedIndexFromSync({
521
+ index: highlightedIndexRef.current
522
+ });
464
523
  return;
465
524
  }
466
525
  const itemIndex = filteredOptions.findIndex(optionItem => isOptionEqualToValue(optionItem, valueItem));
@@ -495,7 +554,7 @@ function useAutocomplete(props) {
495
554
  filteredOptions.length,
496
555
  // Don't sync the highlighted index with the value when multiple
497
556
  // eslint-disable-next-line react-hooks/exhaustive-deps
498
- multiple ? false : value, changeHighlightedIndex, setHighlightedIndex, popupOpen, inputValue, multiple]);
557
+ multiple ? false : value, changeHighlightedIndex, setHighlightedIndex, setHighlightedIndexFromSync, popupOpen, inputValue, multiple]);
499
558
  const handleListboxRef = (0, _useEventCallback.default)(node => {
500
559
  (0, _setRef.default)(listboxRef, node);
501
560
  if (!node) {
@@ -541,6 +600,7 @@ function useAutocomplete(props) {
541
600
  }
542
601
  setOpenState(true);
543
602
  setInputPristine(true);
603
+ isTouchRef.current = false;
544
604
  if (onOpen) {
545
605
  onOpen(event);
546
606
  }
@@ -550,6 +610,8 @@ function useAutocomplete(props) {
550
610
  return;
551
611
  }
552
612
  setOpenState(false);
613
+ touchScrolledRef.current = false;
614
+ highlightReasonRef.current = null;
553
615
  if (onClose) {
554
616
  onClose(event, reason);
555
617
  }
@@ -567,7 +629,6 @@ function useAutocomplete(props) {
567
629
  }
568
630
  setValueState(newValue);
569
631
  };
570
- const isTouch = React.useRef(false);
571
632
  const selectNewValue = (event, option, reasonProp = 'selectOption', origin = 'options') => {
572
633
  let reason = reasonProp;
573
634
  let newValue = option;
@@ -594,7 +655,7 @@ function useAutocomplete(props) {
594
655
  if (!disableCloseOnSelect && (!event || !event.ctrlKey && !event.metaKey)) {
595
656
  handleClose(event, reason);
596
657
  }
597
- if (blurOnSelect === true || blurOnSelect === 'touch' && isTouch.current || blurOnSelect === 'mouse' && !isTouch.current) {
658
+ if (blurOnSelect === true || blurOnSelect === 'touch' && isTouchRef.current || blurOnSelect === 'mouse' && !isTouchRef.current) {
598
659
  inputRef.current.blur();
599
660
  }
600
661
  };
@@ -652,7 +713,6 @@ function useAutocomplete(props) {
652
713
  focusItem(nextItem);
653
714
  };
654
715
  const handleClear = event => {
655
- ignoreFocus.current = true;
656
716
  setInputValueState('');
657
717
  if (onInputChange) {
658
718
  onInputChange(event, '', 'clear');
@@ -780,29 +840,47 @@ function useAutocomplete(props) {
780
840
  }
781
841
  break;
782
842
  case 'Enter':
783
- if (highlightedIndexRef.current !== -1 && popupOpen) {
784
- const option = filteredOptions[highlightedIndexRef.current];
785
- const disabled = getOptionDisabled ? getOptionDisabled(option) : false;
843
+ {
844
+ // In freeSolo, only select the highlighted option if the user hasn't
845
+ // typed new text (inputPristine) or explicitly interacted with an option
846
+ // (keyboard, mouse, or touch — any non-null reason). This lets typed
847
+ // text win over a programmatic highlight (reason=null, e.g. from
848
+ // syncHighlightedIndex matching a previous value) while still honoring
849
+ // deliberate user interactions like hovering a suggestion then pressing Enter.
850
+ const shouldSelectHighlighted = !freeSolo || inputPristine || highlightReasonRef.current !== null;
851
+ if (highlightedIndexRef.current !== -1 && popupOpen && shouldSelectHighlighted &&
852
+ // After a touch-scroll the highlight is stale (the user scrolled
853
+ // past it), so skip selection until the next deliberate interaction.
854
+ !touchScrolledRef.current) {
855
+ const option = filteredOptions[highlightedIndexRef.current];
856
+ const disabled = getOptionDisabled ? getOptionDisabled(option) : false;
786
857
 
787
- // Avoid early form validation, let the end-users continue filling the form.
788
- event.preventDefault();
789
- if (disabled) {
790
- return;
791
- }
792
- selectNewValue(event, option, 'selectOption');
858
+ // Avoid early form validation, let the end-users continue filling the form.
859
+ event.preventDefault();
860
+ if (disabled) {
861
+ return;
862
+ }
863
+ selectNewValue(event, option, 'selectOption');
793
864
 
794
- // Move the selection to the end.
795
- if (autoComplete) {
796
- inputRef.current.setSelectionRange(inputRef.current.value.length, inputRef.current.value.length);
797
- }
798
- } else if (freeSolo && inputValue !== '' && inputValueIsSelectedValue === false) {
799
- if (multiple) {
800
- // Allow people to add new values before they submit the form.
865
+ // Move the selection to the end.
866
+ if (autoComplete) {
867
+ inputRef.current.setSelectionRange(inputRef.current.value.length, inputRef.current.value.length);
868
+ }
869
+ } else if (freeSolo && inputValue !== '' && inputValueIsSelectedValue === false) {
870
+ if (multiple) {
871
+ // Allow people to add new values before they submit the form.
872
+ event.preventDefault();
873
+ }
874
+ selectNewValue(event, inputValue, 'createOption', 'freeSolo');
875
+ } else if (popupOpen && touchScrolledRef.current) {
876
+ // The highlight is stale from a touch-scroll - close without selecting.
801
877
  event.preventDefault();
878
+ // This happens on Enter, but re-using "escape" as the closest `AutocompleteCloseReason`
879
+ // to avoid creating a new reason
880
+ handleClose(event, 'escape');
802
881
  }
803
- selectNewValue(event, inputValue, 'createOption', 'freeSolo');
882
+ break;
804
883
  }
805
- break;
806
884
  case 'Escape':
807
885
  if (popupOpen) {
808
886
  // Avoid Opera to exit fullscreen mode.
@@ -880,7 +958,7 @@ function useAutocomplete(props) {
880
958
  }
881
959
  };
882
960
  const handleBlur = event => {
883
- // Ignore the event when using the scrollbar with IE11
961
+ // Ignore the event when using the scrollbar with IE 11
884
962
  if (unstable_isActiveElementInListbox(listboxRef)) {
885
963
  inputRef.current.focus();
886
964
  return;
@@ -888,7 +966,12 @@ function useAutocomplete(props) {
888
966
  setFocused(false);
889
967
  firstFocus.current = true;
890
968
  ignoreFocus.current = false;
891
- if (autoSelect && highlightedIndexRef.current !== -1 && popupOpen) {
969
+
970
+ // Auto-select the highlighted option on blur, but only if the highlight
971
+ // came from keyboard navigation or was set programmatically (autoHighlight).
972
+ // Mouse hover and touch should not trigger selection — the user may have
973
+ // moved the pointer over an option without intending to commit to it.
974
+ if (autoSelect && highlightedIndexRef.current !== -1 && popupOpen && highlightReasonRef.current !== 'mouse' && highlightReasonRef.current !== 'touch') {
892
975
  selectNewValue(event, filteredOptions[highlightedIndexRef.current], 'blur');
893
976
  } else if (autoSelect && freeSolo && inputValue !== '') {
894
977
  selectNewValue(event, inputValue, 'blur', 'freeSolo');
@@ -899,9 +982,10 @@ function useAutocomplete(props) {
899
982
  };
900
983
  const handleInputChange = event => {
901
984
  const newValue = event.target.value;
902
- if (inputValue !== newValue) {
985
+ const valueChanged = inputValue !== newValue;
986
+ if (valueChanged) {
903
987
  setInputValueState(newValue);
904
- setInputPristine(false);
988
+ touchScrolledRef.current = false;
905
989
  if (onInputChange) {
906
990
  onInputChange(event, newValue, 'input');
907
991
  }
@@ -915,6 +999,12 @@ function useAutocomplete(props) {
915
999
  } else {
916
1000
  handleOpen(event);
917
1001
  }
1002
+
1003
+ // Called after handleOpen so it overrides handleOpen's setInputPristine(true)
1004
+ // when the first keystroke also opens the popup.
1005
+ if (valueChanged) {
1006
+ setInputPristine(false);
1007
+ }
918
1008
  };
919
1009
  const handleOptionMouseMove = event => {
920
1010
  const index = Number(event.currentTarget.getAttribute('data-option-index'));
@@ -924,20 +1014,35 @@ function useAutocomplete(props) {
924
1014
  index,
925
1015
  reason: 'mouse'
926
1016
  });
1017
+ } else {
1018
+ // The option is already highlighted (e.g. programmatically via autoHighlight),
1019
+ // but the user moved the mouse over it — mark as mouse-initiated so
1020
+ // autoSelect on blur correctly treats this as incidental hover.
1021
+ highlightReasonRef.current = 'mouse';
1022
+ }
1023
+ // Don't clear the touch-scroll guard while touch state is still latched.
1024
+ // After a touch gesture, browsers may fire compatibility mousemove
1025
+ // events; if those cleared the guard immediately, later compat events in
1026
+ // the same sequence could be misclassified as a real mouse interaction.
1027
+ // Touch state is cleared by the next deliberate interaction
1028
+ // (keyboard nav, handleOptionClick, or handleOpen).
1029
+ if (!isTouchRef.current) {
1030
+ touchScrolledRef.current = false;
927
1031
  }
928
1032
  };
929
1033
  const handleOptionTouchStart = event => {
1034
+ touchScrolledRef.current = false;
930
1035
  setHighlightedIndex({
931
1036
  event,
932
1037
  index: Number(event.currentTarget.getAttribute('data-option-index')),
933
1038
  reason: 'touch'
934
1039
  });
935
- isTouch.current = true;
1040
+ isTouchRef.current = true;
936
1041
  };
937
1042
  const handleOptionClick = event => {
938
1043
  const index = Number(event.currentTarget.getAttribute('data-option-index'));
939
1044
  selectNewValue(event, filteredOptions[index], 'selectOption');
940
- isTouch.current = false;
1045
+ isTouchRef.current = false;
941
1046
  };
942
1047
  const handleItemDelete = index => event => {
943
1048
  const newValue = value.slice();
@@ -962,11 +1067,11 @@ function useAutocomplete(props) {
962
1067
  // Prevent input blur when interacting with the combobox
963
1068
  const handleMouseDown = event => {
964
1069
  // Prevent focusing the input if click is anywhere outside the Autocomplete
965
- if (!event.currentTarget.contains(event.target)) {
1070
+ if (!(0, _contains.default)(event.currentTarget, event.target)) {
966
1071
  return;
967
1072
  }
968
1073
  // Don't interfere with interactions outside the input area (e.g. helper text)
969
- if (anchorEl && !anchorEl.contains(event.target)) {
1074
+ if (anchorEl && !(0, _contains.default)(anchorEl, event.target)) {
970
1075
  return;
971
1076
  }
972
1077
  if (event.target.getAttribute('id') !== id) {
@@ -977,11 +1082,11 @@ function useAutocomplete(props) {
977
1082
  // Focus the input when interacting with the combobox
978
1083
  const handleClick = event => {
979
1084
  // Prevent focusing the input if click is anywhere outside the Autocomplete
980
- if (!event.currentTarget.contains(event.target)) {
1085
+ if (!(0, _contains.default)(event.currentTarget, event.target)) {
981
1086
  return;
982
1087
  }
983
1088
  // Don't interfere with interactions outside the input area (e.g. helper text)
984
- if (anchorEl && !anchorEl.contains(event.target)) {
1089
+ if (anchorEl && !(0, _contains.default)(anchorEl, event.target)) {
985
1090
  return;
986
1091
  }
987
1092
  inputRef.current.focus();
@@ -1065,7 +1170,10 @@ function useAutocomplete(props) {
1065
1170
  getClearProps: () => ({
1066
1171
  tabIndex: -1,
1067
1172
  type: 'button',
1068
- onClick: handleClear
1173
+ onClick: event => {
1174
+ ignoreFocus.current = true;
1175
+ handleClear(event);
1176
+ }
1069
1177
  }),
1070
1178
  getItemProps: ({
1071
1179
  index = 0
@@ -1093,6 +1201,11 @@ function useAutocomplete(props) {
1093
1201
  onMouseDown: event => {
1094
1202
  // Prevent blur
1095
1203
  event.preventDefault();
1204
+ },
1205
+ onScroll: () => {
1206
+ if (isTouchRef.current) {
1207
+ touchScrolledRef.current = true;
1208
+ }
1096
1209
  }
1097
1210
  }),
1098
1211
  getOptionProps: ({