Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/autocomplete-blur-suggestion.md
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions packages/react/src/Autocomplete/Autocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<>
<LabelledAutocomplete
menuProps={{items: mockItems, selectedItemIds: [], ['aria-labelledby']: 'autocompleteLabel'}}
/>
<button type="button">outside</button>
</>,
)
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(
<LabelledAutocomplete
Expand Down
22 changes: 20 additions & 2 deletions packages/react/src/Autocomplete/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,18 @@ const AutocompleteInput = React.forwardRef(
safeSetTimeout(() => {
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<HTMLInputElement> = event => {
Expand Down Expand Up @@ -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) {
Expand Down
Loading