diff --git a/.changeset/autocomplete-blur-suggestion.md b/.changeset/autocomplete-blur-suggestion.md new file mode 100644 index 00000000000..f05c0a4c428 --- /dev/null +++ b/.changeset/autocomplete-blur-suggestion.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Autocomplete: Keep the typed text instead of restoring the full inline suggestion when the input loses focus, matching the behavior of pressing Escape diff --git a/packages/react/src/Autocomplete/Autocomplete.test.tsx b/packages/react/src/Autocomplete/Autocomplete.test.tsx index 68e61313b3e..ab1d46a7e6d 100644 --- a/packages/react/src/Autocomplete/Autocomplete.test.tsx +++ b/packages/react/src/Autocomplete/Autocomplete.test.tsx @@ -214,6 +214,30 @@ describe('Autocomplete', () => { expect(inputNode?.getAttribute('aria-expanded')).not.toBe('true') }) + it('does not restore the autocomplete suggestion when the input is blurred', async () => { + const user = userEvent.setup() + const {container} = render( + <> + + + , + ) + const inputNode = container.querySelector('#autocompleteInput') as HTMLInputElement + const outsideButton = screen.getByRole('button', {name: 'outside'}) + + // Type 'ze' which gets the inline autocomplete suggestion 'zero' + await user.type(inputNode, 'ze') + expect(inputNode.value).toBe('zero') + + // Move focus elsewhere on the page, like clicking outside the Autocomplete + await user.click(outsideButton) + + // The input should retain the text the user typed rather than the full suggestion + await waitFor(() => expect(inputNode.value).toBe('ze')) + }) + it('allows the value to be 0', () => { const {getByDisplayValue} = render( { if (document.activeElement !== inputRef.current) { setShowMenu(false) + + // Reset the input's value to the text the user actually typed rather than leaving the + // inline autocomplete suggestion in place. This keeps the blur behavior consistent with + // pressing Escape, so deleting characters off a selection and then clicking away does not + // silently restore the full suggestion. See https://github.com/primer/react/issues/4275 + if (inputRef.current && autocompleteSuggestion && inputRef.current.value !== inputValue) { + inputRef.current.value = inputValue + } } }, 0) }, - [onBlur, setShowMenu, inputRef, safeSetTimeout], + [onBlur, setShowMenu, inputRef, safeSetTimeout, autocompleteSuggestion, inputValue], ) const handleInputChange: ChangeEventHandler = event => { @@ -136,7 +144,17 @@ const AutocompleteInput = React.forwardRef( // TODO: fix bug where this function prevents `onChange` from being triggered if the highlighted item text // is the same as what I'm typing // e.g.: typing 'tw' highlights 'two', but when I 'two', the text input change does not get triggered - if (highlightRemainingText && autocompleteSuggestion && (inputValue || isMenuDirectlyActivated)) { + // Only apply the inline autocomplete suggestion while the input is focused. Without this guard, + // the suggestion can be re-applied to the DOM after the input is blurred, which would restore + // the full suggestion the user was editing away from. See https://github.com/primer/react/issues/4275 + const isInputFocused = document.activeElement === inputRef.current + + if ( + isInputFocused && + highlightRemainingText && + autocompleteSuggestion && + (inputValue || isMenuDirectlyActivated) + ) { inputRef.current.value = autocompleteSuggestion if (autocompleteSuggestion.toLowerCase().indexOf(inputValue.toLowerCase()) === 0) {