diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.test.js b/packages/mui-material/src/Autocomplete/Autocomplete.test.js index 228b812abd0911..42a32a6ee3e8e2 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.test.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.test.js @@ -1389,6 +1389,47 @@ describe('', () => { expect(handleChange.args[0][1]).to.deep.equal([]); }); + it('should not suppress focus events after clearing with Escape', async () => { + const handleOpen = spy(); + const { user } = render( + {}} + onOpen={handleOpen} + renderInput={(params) => } + />, + ); + + const textbox = screen.getByRole('combobox'); + + // Opening on initial focus + expect(handleOpen.callCount).to.equal(1); + + // Close the popup first so Escape takes the clear path + await user.keyboard('{Escape}'); + // Popup was open, so first Escape closes it + handleOpen.resetHistory(); + + // Now Escape should clear (popup is closed, value is non-empty) + await user.keyboard('{Escape}'); + + // Focus is still on the input + expect(textbox).toHaveFocus(); + + // Blur and re-focus: onOpen should be called (ignoreFocus was NOT set) + act(() => { + textbox.blur(); + }); + act(() => { + textbox.focus(); + }); + expect(handleOpen.callCount).to.equal(1); + }); + it('should clear on escape if rendering single value', () => { const handleChange = spy(); render( @@ -1634,6 +1675,34 @@ describe('', () => { fireEvent.focus(textbox); expect(textbox).to.have.attribute('aria-expanded', 'true'); }); + + it('should suppress focus events when clearing with the clear button', async () => { + const handleOpen = spy(); + const { user } = render( + {}} + onOpen={handleOpen} + renderInput={(params) => } + />, + ); + + // Opening on initial focus + expect(handleOpen.callCount).to.equal(1); + + // Close popup + await user.keyboard('{Escape}'); + handleOpen.resetHistory(); + + // Click the clear button + const clearButton = screen.getByTitle('Clear'); + await user.click(clearButton); + + // onOpen should NOT be called because ignoreFocus is set + expect(handleOpen.callCount).to.equal(0); + }); }); describe('listbox wrapping behavior', () => { @@ -2576,6 +2645,89 @@ describe('', () => { }); describe('prop: freeSolo', () => { + it('should reset input when controlled value changes to null', async () => { + function App() { + const [value, setValue] = React.useState('foo'); + return ( + + setValue(newValue)} + renderInput={(params) => } + /> + + + ); + } + const { user } = render(); + const textbox = screen.getByRole('combobox'); + expect(textbox.value).to.equal('foo'); + + await user.click(screen.getByRole('button', { name: 'Reset' })); + expect(textbox.value).to.equal(''); + }); + + it('should reset input when controlled value changes to null with clearOnBlur=false', async () => { + function App() { + const [value, setValue] = React.useState('foo'); + return ( + + setValue(newValue)} + renderInput={(params) => } + /> + + + ); + } + const { user } = render(); + const textbox = screen.getByRole('combobox'); + expect(textbox.value).to.equal('foo'); + + await user.click(screen.getByRole('button', { name: 'Reset' })); + expect(textbox.value).to.equal(''); + }); + + it('should retain input when controlled multiple value changes with clearOnBlur=false', async () => { + function App() { + const [value, setValue] = React.useState(['one']); + return ( + + setValue(newValue)} + renderInput={(params) => } + /> + + + ); + } + const { user } = render(); + const textbox = screen.getByRole('combobox'); + + await user.type(textbox, 'abc'); + expect(textbox.value).to.equal('abc'); + + await user.click(screen.getByRole('button', { name: 'Reset' })); + expect(textbox.value).to.equal('abc'); + }); + it('pressing twice enter should not call onChange listener twice', () => { const handleChange = spy(); const options = [{ name: 'foo' }]; diff --git a/packages/mui-material/src/useAutocomplete/useAutocomplete.js b/packages/mui-material/src/useAutocomplete/useAutocomplete.js index cdbe646f296be4..b0124d6ef5752d 100644 --- a/packages/mui-material/src/useAutocomplete/useAutocomplete.js +++ b/packages/mui-material/src/useAutocomplete/useAutocomplete.js @@ -178,10 +178,12 @@ function useAutocomplete(props) { const resetInputValue = React.useCallback( (event, newValue, reason) => { - // retain current `inputValue` if new option isn't selected and `clearOnBlur` is false - // When `multiple` is enabled, `newValue` is an array of all selected items including the newly selected item + // Retain the current `inputValue` when no new option is selected and `clearOnBlur` is false. + // In `multiple` mode, `newValue` is the next value array, so only length growth counts as a selection. const isOptionSelected = multiple ? value.length < newValue.length : newValue !== null; - if (!isOptionSelected && !clearOnBlur) { + // A controlled single-value `freeSolo` reset to `null` should still clear the input. + const shouldClearOnReset = reason === 'reset' && freeSolo && !multiple && newValue === null; + if (!isOptionSelected && !clearOnBlur && !shouldClearOnReset) { return; } const newInputValue = getInputValue(newValue, multiple, getOptionLabel, renderValue); @@ -203,6 +205,7 @@ function useAutocomplete(props) { onInputChange, setInputValueState, clearOnBlur, + freeSolo, value, renderValue, ], @@ -826,7 +829,6 @@ function useAutocomplete(props) { }; const handleClear = (event) => { - ignoreFocus.current = true; setInputValueState(''); if (onInputChange) { @@ -1272,7 +1274,10 @@ function useAutocomplete(props) { getClearProps: () => ({ tabIndex: -1, type: 'button', - onClick: handleClear, + onClick: (event) => { + ignoreFocus.current = true; + handleClear(event); + }, }), getItemProps: ({ index = 0 } = {}) => ({ ...(multiple && { key: index }),