@mui/material 7.3.10 → 7.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Autocomplete/Autocomplete.js +63 -12
- package/Button/Button.js +18 -1
- package/CHANGELOG.md +35 -0
- package/Checkbox/Checkbox.js +2 -1
- package/ClickAwayListener/ClickAwayListener.js +2 -5
- package/Dialog/Dialog.js +11 -6
- package/Drawer/Drawer.js +18 -4
- package/Fab/Fab.js +7 -1
- package/InputBase/InputBase.js +39 -7
- package/ListItemButton/ListItemButton.js +7 -1
- package/MenuItem/MenuItem.js +6 -1
- package/Popper/BasePopper.js +23 -1
- package/Slider/useSlider.js +6 -3
- package/SwipeableDrawer/SwipeableDrawer.js +5 -4
- package/Switch/Switch.js +3 -4
- package/Unstable_TrapFocus/FocusTrap.js +15 -5
- package/esm/Autocomplete/Autocomplete.js +63 -12
- package/esm/Button/Button.js +18 -1
- package/esm/Checkbox/Checkbox.js +2 -1
- package/esm/ClickAwayListener/ClickAwayListener.js +2 -5
- package/esm/Dialog/Dialog.js +11 -6
- package/esm/Drawer/Drawer.js +18 -4
- package/esm/Fab/Fab.js +7 -1
- package/esm/InputBase/InputBase.js +39 -7
- package/esm/ListItemButton/ListItemButton.js +7 -1
- package/esm/MenuItem/MenuItem.js +6 -1
- package/esm/Popper/BasePopper.js +23 -1
- package/esm/Slider/useSlider.js +6 -3
- package/esm/SwipeableDrawer/SwipeableDrawer.js +5 -4
- package/esm/Switch/Switch.js +3 -4
- package/esm/Unstable_TrapFocus/FocusTrap.js +15 -5
- package/esm/index.js +1 -1
- package/esm/useAutocomplete/useAutocomplete.d.ts +4 -5
- package/esm/useAutocomplete/useAutocomplete.js +155 -46
- package/esm/utils/contains.d.ts +2 -0
- package/esm/utils/contains.js +2 -0
- package/esm/utils/focusable.d.ts +7 -0
- package/esm/utils/focusable.js +13 -0
- package/esm/utils/getEventTarget.d.ts +2 -0
- package/esm/utils/getEventTarget.js +2 -0
- package/esm/version/index.js +2 -2
- package/index.js +1 -1
- package/package.json +5 -5
- package/useAutocomplete/useAutocomplete.d.ts +4 -5
- package/useAutocomplete/useAutocomplete.js +155 -46
- package/utils/contains.d.ts +2 -0
- package/utils/contains.js +9 -0
- package/utils/focusable.d.ts +7 -0
- package/utils/focusable.js +20 -0
- package/utils/getEventTarget.d.ts +2 -0
- package/utils/getEventTarget.js +9 -0
- package/version/index.js +2 -2
|
@@ -8,7 +8,9 @@ import ownerDocument from '@mui/utils/ownerDocument';
|
|
|
8
8
|
import getReactElementRef from '@mui/utils/getReactElementRef';
|
|
9
9
|
import exactProp from '@mui/utils/exactProp';
|
|
10
10
|
import elementAcceptingRef from '@mui/utils/elementAcceptingRef';
|
|
11
|
+
import contains from "../utils/contains.js";
|
|
11
12
|
import getActiveElement from "../utils/getActiveElement.js";
|
|
13
|
+
import { getFocusTarget } from "../utils/focusable.js";
|
|
12
14
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
15
|
// Inspired by https://github.com/focus-trap/tabbable
|
|
14
16
|
const candidatesSelector = ['input', 'select', 'textarea', 'a[href]', 'button', '[tabindex]', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])'].join(',');
|
|
@@ -107,21 +109,29 @@ function FocusTrap(props) {
|
|
|
107
109
|
activated.current = !disableAutoFocus;
|
|
108
110
|
}, [disableAutoFocus, open]);
|
|
109
111
|
React.useEffect(() => {
|
|
112
|
+
// Reset on every mount — React 18 Strict Mode double-mounts leave this
|
|
113
|
+
// stuck at `true` after the cleanup of the previous mount set it.
|
|
114
|
+
ignoreNextEnforceFocus.current = false;
|
|
115
|
+
|
|
110
116
|
// We might render an empty child.
|
|
111
117
|
if (!open || !rootRef.current) {
|
|
112
118
|
return;
|
|
113
119
|
}
|
|
114
120
|
const doc = ownerDocument(rootRef.current);
|
|
115
121
|
const activeElement = getActiveElement(doc);
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
|
|
123
|
+
// Prefer the explicitly marked focusable element. Fall back to the root
|
|
124
|
+
// element for generic FocusTrap usage.
|
|
125
|
+
const focusTarget = getFocusTarget(rootRef.current) ?? rootRef.current;
|
|
126
|
+
if (!contains(rootRef.current, activeElement)) {
|
|
127
|
+
if (!focusTarget.hasAttribute('tabIndex')) {
|
|
118
128
|
if (process.env.NODE_ENV !== 'production') {
|
|
119
129
|
console.error(['MUI: The modal content node does not accept focus.', 'For the benefit of assistive technologies, ' + 'the tabIndex of the node is being set to "-1".'].join('\n'));
|
|
120
130
|
}
|
|
121
|
-
|
|
131
|
+
focusTarget.setAttribute('tabIndex', '-1');
|
|
122
132
|
}
|
|
123
133
|
if (activated.current) {
|
|
124
|
-
|
|
134
|
+
focusTarget.focus();
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
137
|
return () => {
|
|
@@ -181,7 +191,7 @@ function FocusTrap(props) {
|
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
// The focus is already inside
|
|
184
|
-
if (
|
|
194
|
+
if (contains(rootElement, activeEl)) {
|
|
185
195
|
return;
|
|
186
196
|
}
|
|
187
197
|
|
package/esm/index.js
CHANGED
|
@@ -46,12 +46,11 @@ export interface UseAutocompleteProps<Value, Multiple extends boolean | undefine
|
|
|
46
46
|
*/
|
|
47
47
|
autoHighlight?: boolean | undefined;
|
|
48
48
|
/**
|
|
49
|
-
* If `true`, the
|
|
50
|
-
* when the Autocomplete loses focus unless the user chooses
|
|
51
|
-
* a different option or changes the character string in the input.
|
|
49
|
+
* If `true`, the value is updated when the input loses focus under one of these conditions:
|
|
52
50
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
51
|
+
* - An option highlighted via keyboard navigation or `autoHighlight` is selected.
|
|
52
|
+
* Hover and touch highlights are ignored.
|
|
53
|
+
* - Otherwise, in `freeSolo` mode, the typed text becomes the value.
|
|
55
54
|
* @default false
|
|
56
55
|
*/
|
|
57
56
|
autoSelect?: boolean | undefined;
|
|
@@ -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
|
|
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) {
|
|
@@ -133,6 +134,20 @@ function useAutocomplete(props) {
|
|
|
133
134
|
const defaultHighlighted = autoHighlight ? 0 : -1;
|
|
134
135
|
const highlightedIndexRef = React.useRef(defaultHighlighted);
|
|
135
136
|
|
|
137
|
+
// Tracks how the current highlight was set:
|
|
138
|
+
// - 'keyboard' — arrow keys, Home/End, PageUp/PageDown
|
|
139
|
+
// - 'mouse' — handleOptionMouseMove
|
|
140
|
+
// - 'touch' — handleOptionTouchStart
|
|
141
|
+
// - null — programmatic (autoHighlight, value sync)
|
|
142
|
+
//
|
|
143
|
+
// This lets handleBlur and the Enter handler distinguish intentional
|
|
144
|
+
// interactions from incidental ones — e.g. autoSelect should not commit
|
|
145
|
+
// a highlight that came from a casual mouse hover.
|
|
146
|
+
/** @type {React.RefObject<AutocompleteHighlightChangeReason | null>} */
|
|
147
|
+
const highlightReasonRef = React.useRef(null);
|
|
148
|
+
const touchScrolledRef = React.useRef(false);
|
|
149
|
+
const isTouchRef = React.useRef(false);
|
|
150
|
+
|
|
136
151
|
// Calculate the initial inputValue on mount only.
|
|
137
152
|
// useRef ensures it doesn't update dynamically with defaultValue or value props.
|
|
138
153
|
const initialInputValue = React.useRef(getInputValue(defaultValue ?? valueProp, multiple, getOptionLabel)).current;
|
|
@@ -272,22 +287,17 @@ function useAutocomplete(props) {
|
|
|
272
287
|
}
|
|
273
288
|
}
|
|
274
289
|
}
|
|
275
|
-
const
|
|
276
|
-
event,
|
|
290
|
+
const syncHighlightedIndexToDOM = useEventCallback(({
|
|
277
291
|
index,
|
|
278
|
-
reason
|
|
292
|
+
reason,
|
|
293
|
+
preserveScroll = false
|
|
279
294
|
}) => {
|
|
280
|
-
highlightedIndexRef.current = index;
|
|
281
|
-
|
|
282
295
|
// does the index exist?
|
|
283
296
|
if (index === -1) {
|
|
284
297
|
inputRef.current.removeAttribute('aria-activedescendant');
|
|
285
298
|
} else {
|
|
286
299
|
inputRef.current.setAttribute('aria-activedescendant', `${id}-option-${index}`);
|
|
287
300
|
}
|
|
288
|
-
if (onHighlightChange && ['mouse', 'keyboard', 'touch'].includes(reason)) {
|
|
289
|
-
onHighlightChange(event, index === -1 ? null : filteredOptions[index], reason);
|
|
290
|
-
}
|
|
291
301
|
if (!listboxRef.current) {
|
|
292
302
|
return;
|
|
293
303
|
}
|
|
@@ -306,7 +316,9 @@ function useAutocomplete(props) {
|
|
|
306
316
|
return;
|
|
307
317
|
}
|
|
308
318
|
if (index === -1) {
|
|
309
|
-
|
|
319
|
+
if (!preserveScroll) {
|
|
320
|
+
listboxNode.scrollTop = 0;
|
|
321
|
+
}
|
|
310
322
|
return;
|
|
311
323
|
}
|
|
312
324
|
const option = listboxRef.current.querySelector(`[data-option-index="${index}"]`);
|
|
@@ -334,15 +346,46 @@ function useAutocomplete(props) {
|
|
|
334
346
|
}
|
|
335
347
|
}
|
|
336
348
|
});
|
|
349
|
+
const setHighlightedIndex = useEventCallback(({
|
|
350
|
+
event,
|
|
351
|
+
index,
|
|
352
|
+
reason,
|
|
353
|
+
preserveScroll = false
|
|
354
|
+
}) => {
|
|
355
|
+
highlightedIndexRef.current = index;
|
|
356
|
+
highlightReasonRef.current = reason ?? null;
|
|
357
|
+
if (onHighlightChange && ['mouse', 'keyboard', 'touch'].includes(reason)) {
|
|
358
|
+
onHighlightChange(event, index === -1 ? null : filteredOptions[index], reason);
|
|
359
|
+
}
|
|
360
|
+
syncHighlightedIndexToDOM({
|
|
361
|
+
index,
|
|
362
|
+
reason,
|
|
363
|
+
preserveScroll
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
const setHighlightedIndexFromSync = useEventCallback(({
|
|
367
|
+
index
|
|
368
|
+
}) => {
|
|
369
|
+
highlightedIndexRef.current = index;
|
|
370
|
+
syncHighlightedIndexToDOM({
|
|
371
|
+
index,
|
|
372
|
+
reason: highlightReasonRef.current
|
|
373
|
+
});
|
|
374
|
+
});
|
|
337
375
|
const changeHighlightedIndex = useEventCallback(({
|
|
338
376
|
event,
|
|
339
377
|
diff,
|
|
340
378
|
direction = 'next',
|
|
341
|
-
reason
|
|
379
|
+
reason,
|
|
380
|
+
preserveScroll
|
|
342
381
|
}) => {
|
|
343
382
|
if (!popupOpen) {
|
|
344
383
|
return;
|
|
345
384
|
}
|
|
385
|
+
if (reason === 'keyboard') {
|
|
386
|
+
touchScrolledRef.current = false;
|
|
387
|
+
isTouchRef.current = false;
|
|
388
|
+
}
|
|
346
389
|
const getNextIndex = () => {
|
|
347
390
|
const maxIndex = filteredOptions.length - 1;
|
|
348
391
|
if (diff === 'reset') {
|
|
@@ -379,7 +422,8 @@ function useAutocomplete(props) {
|
|
|
379
422
|
setHighlightedIndex({
|
|
380
423
|
index: nextIndex,
|
|
381
424
|
reason,
|
|
382
|
-
event
|
|
425
|
+
event,
|
|
426
|
+
preserveScroll
|
|
383
427
|
});
|
|
384
428
|
|
|
385
429
|
// Sync the content of the input with the highlighted option.
|
|
@@ -433,15 +477,24 @@ function useAutocomplete(props) {
|
|
|
433
477
|
// If it exists and the value and the inputValue haven't changed, just update its index, otherwise continue execution
|
|
434
478
|
const previousHighlightedOptionIndex = getPreviousHighlightedOptionIndex();
|
|
435
479
|
if (previousHighlightedOptionIndex !== -1) {
|
|
436
|
-
|
|
480
|
+
// Keep the original highlight reason while re-syncing the DOM state.
|
|
481
|
+
// The highlighted option still exists after the filteredOptions array changed
|
|
482
|
+
// (e.g. async fetch returns new options while the user is mid-navigation),
|
|
483
|
+
// so the original interaction reason (keyboard, mouse, etc.) still applies.
|
|
484
|
+
setHighlightedIndexFromSync({
|
|
485
|
+
index: previousHighlightedOptionIndex
|
|
486
|
+
});
|
|
437
487
|
return;
|
|
438
488
|
}
|
|
439
489
|
const valueItem = multiple ? value[0] : value;
|
|
440
490
|
|
|
441
491
|
// The popup is empty, reset
|
|
442
492
|
if (filteredOptions.length === 0 || valueItem == null) {
|
|
493
|
+
// Preserve scroll when new options are appended without changing the current filter.
|
|
494
|
+
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]));
|
|
443
495
|
changeHighlightedIndex({
|
|
444
|
-
diff: 'reset'
|
|
496
|
+
diff: 'reset',
|
|
497
|
+
preserveScroll: isAppendOnly
|
|
445
498
|
});
|
|
446
499
|
return;
|
|
447
500
|
}
|
|
@@ -453,8 +506,12 @@ function useAutocomplete(props) {
|
|
|
453
506
|
if (valueItem != null) {
|
|
454
507
|
const currentOption = filteredOptions[highlightedIndexRef.current];
|
|
455
508
|
|
|
456
|
-
// Keep the current
|
|
457
|
-
|
|
509
|
+
// Keep the current selected highlight while the popup stays open;
|
|
510
|
+
// on reopen, resync from the selected value.
|
|
511
|
+
if (multiple && currentOption && value.findIndex(val => isOptionEqualToValue(currentOption, val)) !== -1 && previousProps.filteredOptions?.length > 0) {
|
|
512
|
+
setHighlightedIndexFromSync({
|
|
513
|
+
index: highlightedIndexRef.current
|
|
514
|
+
});
|
|
458
515
|
return;
|
|
459
516
|
}
|
|
460
517
|
const itemIndex = filteredOptions.findIndex(optionItem => isOptionEqualToValue(optionItem, valueItem));
|
|
@@ -489,7 +546,7 @@ function useAutocomplete(props) {
|
|
|
489
546
|
filteredOptions.length,
|
|
490
547
|
// Don't sync the highlighted index with the value when multiple
|
|
491
548
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
492
|
-
multiple ? false : value, changeHighlightedIndex, setHighlightedIndex, popupOpen, inputValue, multiple]);
|
|
549
|
+
multiple ? false : value, changeHighlightedIndex, setHighlightedIndex, setHighlightedIndexFromSync, popupOpen, inputValue, multiple]);
|
|
493
550
|
const handleListboxRef = useEventCallback(node => {
|
|
494
551
|
setRef(listboxRef, node);
|
|
495
552
|
if (!node) {
|
|
@@ -535,6 +592,7 @@ function useAutocomplete(props) {
|
|
|
535
592
|
}
|
|
536
593
|
setOpenState(true);
|
|
537
594
|
setInputPristine(true);
|
|
595
|
+
isTouchRef.current = false;
|
|
538
596
|
if (onOpen) {
|
|
539
597
|
onOpen(event);
|
|
540
598
|
}
|
|
@@ -544,6 +602,8 @@ function useAutocomplete(props) {
|
|
|
544
602
|
return;
|
|
545
603
|
}
|
|
546
604
|
setOpenState(false);
|
|
605
|
+
touchScrolledRef.current = false;
|
|
606
|
+
highlightReasonRef.current = null;
|
|
547
607
|
if (onClose) {
|
|
548
608
|
onClose(event, reason);
|
|
549
609
|
}
|
|
@@ -561,7 +621,6 @@ function useAutocomplete(props) {
|
|
|
561
621
|
}
|
|
562
622
|
setValueState(newValue);
|
|
563
623
|
};
|
|
564
|
-
const isTouch = React.useRef(false);
|
|
565
624
|
const selectNewValue = (event, option, reasonProp = 'selectOption', origin = 'options') => {
|
|
566
625
|
let reason = reasonProp;
|
|
567
626
|
let newValue = option;
|
|
@@ -588,7 +647,7 @@ function useAutocomplete(props) {
|
|
|
588
647
|
if (!disableCloseOnSelect && (!event || !event.ctrlKey && !event.metaKey)) {
|
|
589
648
|
handleClose(event, reason);
|
|
590
649
|
}
|
|
591
|
-
if (blurOnSelect === true || blurOnSelect === 'touch' &&
|
|
650
|
+
if (blurOnSelect === true || blurOnSelect === 'touch' && isTouchRef.current || blurOnSelect === 'mouse' && !isTouchRef.current) {
|
|
592
651
|
inputRef.current.blur();
|
|
593
652
|
}
|
|
594
653
|
};
|
|
@@ -777,29 +836,47 @@ function useAutocomplete(props) {
|
|
|
777
836
|
}
|
|
778
837
|
break;
|
|
779
838
|
case 'Enter':
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
839
|
+
{
|
|
840
|
+
// In freeSolo, only select the highlighted option if the user hasn't
|
|
841
|
+
// typed new text (inputPristine) or explicitly interacted with an option
|
|
842
|
+
// (keyboard, mouse, or touch — any non-null reason). This lets typed
|
|
843
|
+
// text win over a programmatic highlight (reason=null, e.g. from
|
|
844
|
+
// syncHighlightedIndex matching a previous value) while still honoring
|
|
845
|
+
// deliberate user interactions like hovering a suggestion then pressing Enter.
|
|
846
|
+
const shouldSelectHighlighted = !freeSolo || inputPristine || highlightReasonRef.current !== null;
|
|
847
|
+
if (highlightedIndexRef.current !== -1 && popupOpen && shouldSelectHighlighted &&
|
|
848
|
+
// After a touch-scroll the highlight is stale (the user scrolled
|
|
849
|
+
// past it), so skip selection until the next deliberate interaction.
|
|
850
|
+
!touchScrolledRef.current) {
|
|
851
|
+
const option = filteredOptions[highlightedIndexRef.current];
|
|
852
|
+
const disabled = getOptionDisabled ? getOptionDisabled(option) : false;
|
|
783
853
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
854
|
+
// Avoid early form validation, let the end-users continue filling the form.
|
|
855
|
+
event.preventDefault();
|
|
856
|
+
if (disabled) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
selectNewValue(event, option, 'selectOption');
|
|
790
860
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
861
|
+
// Move the selection to the end.
|
|
862
|
+
if (autoComplete) {
|
|
863
|
+
inputRef.current.setSelectionRange(inputRef.current.value.length, inputRef.current.value.length);
|
|
864
|
+
}
|
|
865
|
+
} else if (freeSolo && inputValue !== '' && inputValueIsSelectedValue === false) {
|
|
866
|
+
if (multiple) {
|
|
867
|
+
// Allow people to add new values before they submit the form.
|
|
868
|
+
event.preventDefault();
|
|
869
|
+
}
|
|
870
|
+
selectNewValue(event, inputValue, 'createOption', 'freeSolo');
|
|
871
|
+
} else if (popupOpen && touchScrolledRef.current) {
|
|
872
|
+
// The highlight is stale from a touch-scroll - close without selecting.
|
|
798
873
|
event.preventDefault();
|
|
874
|
+
// This happens on Enter, but re-using "escape" as the closest `AutocompleteCloseReason`
|
|
875
|
+
// to avoid creating a new reason
|
|
876
|
+
handleClose(event, 'escape');
|
|
799
877
|
}
|
|
800
|
-
|
|
878
|
+
break;
|
|
801
879
|
}
|
|
802
|
-
break;
|
|
803
880
|
case 'Escape':
|
|
804
881
|
if (popupOpen) {
|
|
805
882
|
// Avoid Opera to exit fullscreen mode.
|
|
@@ -885,7 +962,12 @@ function useAutocomplete(props) {
|
|
|
885
962
|
setFocused(false);
|
|
886
963
|
firstFocus.current = true;
|
|
887
964
|
ignoreFocus.current = false;
|
|
888
|
-
|
|
965
|
+
|
|
966
|
+
// Auto-select the highlighted option on blur, but only if the highlight
|
|
967
|
+
// came from keyboard navigation or was set programmatically (autoHighlight).
|
|
968
|
+
// Mouse hover and touch should not trigger selection — the user may have
|
|
969
|
+
// moved the pointer over an option without intending to commit to it.
|
|
970
|
+
if (autoSelect && highlightedIndexRef.current !== -1 && popupOpen && highlightReasonRef.current !== 'mouse' && highlightReasonRef.current !== 'touch') {
|
|
889
971
|
selectNewValue(event, filteredOptions[highlightedIndexRef.current], 'blur');
|
|
890
972
|
} else if (autoSelect && freeSolo && inputValue !== '') {
|
|
891
973
|
selectNewValue(event, inputValue, 'blur', 'freeSolo');
|
|
@@ -896,9 +978,10 @@ function useAutocomplete(props) {
|
|
|
896
978
|
};
|
|
897
979
|
const handleInputChange = event => {
|
|
898
980
|
const newValue = event.target.value;
|
|
899
|
-
|
|
981
|
+
const valueChanged = inputValue !== newValue;
|
|
982
|
+
if (valueChanged) {
|
|
900
983
|
setInputValueState(newValue);
|
|
901
|
-
|
|
984
|
+
touchScrolledRef.current = false;
|
|
902
985
|
if (onInputChange) {
|
|
903
986
|
onInputChange(event, newValue, 'input');
|
|
904
987
|
}
|
|
@@ -912,6 +995,12 @@ function useAutocomplete(props) {
|
|
|
912
995
|
} else {
|
|
913
996
|
handleOpen(event);
|
|
914
997
|
}
|
|
998
|
+
|
|
999
|
+
// Called after handleOpen so it overrides handleOpen's setInputPristine(true)
|
|
1000
|
+
// when the first keystroke also opens the popup.
|
|
1001
|
+
if (valueChanged) {
|
|
1002
|
+
setInputPristine(false);
|
|
1003
|
+
}
|
|
915
1004
|
};
|
|
916
1005
|
const handleOptionMouseMove = event => {
|
|
917
1006
|
const index = Number(event.currentTarget.getAttribute('data-option-index'));
|
|
@@ -921,20 +1010,35 @@ function useAutocomplete(props) {
|
|
|
921
1010
|
index,
|
|
922
1011
|
reason: 'mouse'
|
|
923
1012
|
});
|
|
1013
|
+
} else {
|
|
1014
|
+
// The option is already highlighted (e.g. programmatically via autoHighlight),
|
|
1015
|
+
// but the user moved the mouse over it — mark as mouse-initiated so
|
|
1016
|
+
// autoSelect on blur correctly treats this as incidental hover.
|
|
1017
|
+
highlightReasonRef.current = 'mouse';
|
|
1018
|
+
}
|
|
1019
|
+
// Don't clear the touch-scroll guard while touch state is still latched.
|
|
1020
|
+
// After a touch gesture, browsers may fire compatibility mousemove
|
|
1021
|
+
// events; if those cleared the guard immediately, later compat events in
|
|
1022
|
+
// the same sequence could be misclassified as a real mouse interaction.
|
|
1023
|
+
// Touch state is cleared by the next deliberate interaction
|
|
1024
|
+
// (keyboard nav, handleOptionClick, or handleOpen).
|
|
1025
|
+
if (!isTouchRef.current) {
|
|
1026
|
+
touchScrolledRef.current = false;
|
|
924
1027
|
}
|
|
925
1028
|
};
|
|
926
1029
|
const handleOptionTouchStart = event => {
|
|
1030
|
+
touchScrolledRef.current = false;
|
|
927
1031
|
setHighlightedIndex({
|
|
928
1032
|
event,
|
|
929
1033
|
index: Number(event.currentTarget.getAttribute('data-option-index')),
|
|
930
1034
|
reason: 'touch'
|
|
931
1035
|
});
|
|
932
|
-
|
|
1036
|
+
isTouchRef.current = true;
|
|
933
1037
|
};
|
|
934
1038
|
const handleOptionClick = event => {
|
|
935
1039
|
const index = Number(event.currentTarget.getAttribute('data-option-index'));
|
|
936
1040
|
selectNewValue(event, filteredOptions[index], 'selectOption');
|
|
937
|
-
|
|
1041
|
+
isTouchRef.current = false;
|
|
938
1042
|
};
|
|
939
1043
|
const handleItemDelete = index => event => {
|
|
940
1044
|
const newValue = value.slice();
|
|
@@ -959,11 +1063,11 @@ function useAutocomplete(props) {
|
|
|
959
1063
|
// Prevent input blur when interacting with the combobox
|
|
960
1064
|
const handleMouseDown = event => {
|
|
961
1065
|
// Prevent focusing the input if click is anywhere outside the Autocomplete
|
|
962
|
-
if (!event.currentTarget
|
|
1066
|
+
if (!contains(event.currentTarget, event.target)) {
|
|
963
1067
|
return;
|
|
964
1068
|
}
|
|
965
1069
|
// Don't interfere with interactions outside the input area (e.g. helper text)
|
|
966
|
-
if (anchorEl && !
|
|
1070
|
+
if (anchorEl && !contains(anchorEl, event.target)) {
|
|
967
1071
|
return;
|
|
968
1072
|
}
|
|
969
1073
|
if (event.target.getAttribute('id') !== id) {
|
|
@@ -974,11 +1078,11 @@ function useAutocomplete(props) {
|
|
|
974
1078
|
// Focus the input when interacting with the combobox
|
|
975
1079
|
const handleClick = event => {
|
|
976
1080
|
// Prevent focusing the input if click is anywhere outside the Autocomplete
|
|
977
|
-
if (!event.currentTarget
|
|
1081
|
+
if (!contains(event.currentTarget, event.target)) {
|
|
978
1082
|
return;
|
|
979
1083
|
}
|
|
980
1084
|
// Don't interfere with interactions outside the input area (e.g. helper text)
|
|
981
|
-
if (anchorEl && !
|
|
1085
|
+
if (anchorEl && !contains(anchorEl, event.target)) {
|
|
982
1086
|
return;
|
|
983
1087
|
}
|
|
984
1088
|
inputRef.current.focus();
|
|
@@ -1103,6 +1207,11 @@ function useAutocomplete(props) {
|
|
|
1103
1207
|
onMouseDown: event => {
|
|
1104
1208
|
// Prevent blur
|
|
1105
1209
|
event.preventDefault();
|
|
1210
|
+
},
|
|
1211
|
+
onScroll: () => {
|
|
1212
|
+
if (isTouchRef.current) {
|
|
1213
|
+
touchScrolledRef.current = true;
|
|
1214
|
+
}
|
|
1106
1215
|
}
|
|
1107
1216
|
}),
|
|
1108
1217
|
getOptionProps: ({
|
|
@@ -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,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
|
+
}
|
package/esm/version/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export const version = "7.3.
|
|
1
|
+
export const version = "7.3.11";
|
|
2
2
|
export const major = Number("7");
|
|
3
3
|
export const minor = Number("3");
|
|
4
|
-
export const patch = Number("
|
|
4
|
+
export const patch = Number("11");
|
|
5
5
|
export const prerelease = undefined;
|
|
6
6
|
export default version;
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/material",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.11",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "Material UI is an open-source React component library that implements Google's Material Design. It's comprehensive and can be used in production out of the box.",
|
|
6
6
|
"keywords": [
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"prop-types": "^15.8.1",
|
|
34
34
|
"react-is": "^19.2.3",
|
|
35
35
|
"react-transition-group": "^4.4.5",
|
|
36
|
-
"@mui/core-downloads-tracker": "^7.3.
|
|
36
|
+
"@mui/core-downloads-tracker": "^7.3.11",
|
|
37
|
+
"@mui/utils": "^7.3.11",
|
|
37
38
|
"@mui/types": "^7.4.12",
|
|
38
|
-
"@mui/
|
|
39
|
-
"@mui/system": "^7.3.10"
|
|
39
|
+
"@mui/system": "^7.3.11"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@emotion/react": "^11.5.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
45
45
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
46
46
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
47
|
-
"@mui/material-pigment-css": "^7.3.
|
|
47
|
+
"@mui/material-pigment-css": "^7.3.11"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
50
50
|
"@types/react": {
|
|
@@ -46,12 +46,11 @@ export interface UseAutocompleteProps<Value, Multiple extends boolean | undefine
|
|
|
46
46
|
*/
|
|
47
47
|
autoHighlight?: boolean | undefined;
|
|
48
48
|
/**
|
|
49
|
-
* If `true`, the
|
|
50
|
-
* when the Autocomplete loses focus unless the user chooses
|
|
51
|
-
* a different option or changes the character string in the input.
|
|
49
|
+
* If `true`, the value is updated when the input loses focus under one of these conditions:
|
|
52
50
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
51
|
+
* - An option highlighted via keyboard navigation or `autoHighlight` is selected.
|
|
52
|
+
* Hover and touch highlights are ignored.
|
|
53
|
+
* - Otherwise, in `freeSolo` mode, the typed text becomes the value.
|
|
55
54
|
* @default false
|
|
56
55
|
*/
|
|
57
56
|
autoSelect?: boolean | undefined;
|