diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.test.js b/packages/mui-material/src/Autocomplete/Autocomplete.test.js index c64005ad1c64da..86b7795bdb7ab5 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.test.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.test.js @@ -3374,6 +3374,73 @@ describe('', () => { expect(handleChange.args[0][1]).to.equal('The'); }); + it('should prevent form submission when committing typed text over auto-highlighted match', async () => { + const handleChange = spy(); + const handleSubmit = spy(); + const options = ['The Shawshank Redemption', 'The Godfather']; + const { user } = render( +
{ + if (!event.defaultPrevented && event.key === 'Enter') { + handleSubmit(); + } + }} + > + } + /> +
, + ); + + await user.type(screen.getByRole('combobox'), 'The{Enter}'); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.equal('The'); + expect(handleSubmit.callCount).to.equal(0); + + await user.keyboard('{Enter}'); + + expect(handleSubmit.callCount).to.equal(1); + }); + + it('should prevent form submission when committing edited selected text over value-highlighted match', async () => { + const handleChange = spy(); + const handleSubmit = spy(); + const options = ['The Shawshank Redemption', 'The Godfather']; + const { user } = render( +
{ + if (!event.defaultPrevented && event.key === 'Enter') { + handleSubmit(); + } + }} + > + } + /> +
, + ); + + await user.keyboard('{Backspace}{Backspace}{Backspace}{Backspace}{Backspace}{Enter}'); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.equal('The Godf'); + expect(handleSubmit.callCount).to.equal(0); + + await user.keyboard('{Enter}'); + + expect(handleSubmit.callCount).to.equal(1); + }); + it('should prefer typed text after editing a selected value', async () => { const handleChange = spy(); const options = ['The Shawshank Redemption', 'The Godfather']; diff --git a/packages/mui-material/src/useAutocomplete/useAutocomplete.js b/packages/mui-material/src/useAutocomplete/useAutocomplete.js index c2251a52b1fbc4..8b524bbc1192bc 100644 --- a/packages/mui-material/src/useAutocomplete/useAutocomplete.js +++ b/packages/mui-material/src/useAutocomplete/useAutocomplete.js @@ -1016,14 +1016,19 @@ function useAutocomplete(props) { } break; case 'Enter': { - // In freeSolo, only select the highlighted option if the user hasn't - // typed new text (inputPristine) or explicitly interacted with an option - // (keyboard, mouse, or touch — any non-null reason). This lets typed - // text win over a programmatic highlight (reason=null, e.g. from - // syncHighlightedIndex matching a previous value) while still honoring - // deliberate user interactions like hovering a suggestion then pressing Enter. + // In freeSolo, once the user has typed new text, Enter should commit that text instead + // of treating a programmatic highlight as an intentional selection. Programmatic + // highlights include autoHighlight and syncHighlightedIndex matching a previous value. + const hasProgrammaticHighlight = + popupOpen && + highlightedIndexRef.current !== -1 && + highlightReasonRef.current === null; + const shouldCommitFreeSoloOverProgrammaticHighlight = + freeSolo && !inputPristine && hasProgrammaticHighlight; const shouldSelectHighlighted = - !freeSolo || inputPristine || highlightReasonRef.current !== null; + !freeSolo || inputPristine || !hasProgrammaticHighlight; + const shouldPreventSubmitAfterFreeSoloCommit = + shouldCommitFreeSoloOverProgrammaticHighlight && !touchScrolledRef.current; if ( highlightedIndexRef.current !== -1 && @@ -1053,8 +1058,8 @@ function useAutocomplete(props) { ); } } else if (freeSolo && inputValue !== '' && inputValueIsSelectedValue === false) { - if (multiple) { - // Allow people to add new values before they submit the form. + if (multiple || shouldPreventSubmitAfterFreeSoloCommit) { + // Allow people to commit the freeSolo value before they submit the form. event.preventDefault(); } selectNewValue(event, inputValue, 'createOption', 'freeSolo');