Skip to content

[autocomplete] Fix highlight sync and scroll preservation#48322

Merged
mj12albert merged 5 commits into
mui:masterfrom
mj12albert:autocomplete/fix-highlight-sync-scroll-preservation-3of5
Apr 22, 2026
Merged

[autocomplete] Fix highlight sync and scroll preservation#48322
mj12albert merged 5 commits into
mui:masterfrom
mj12albert:autocomplete/fix-highlight-sync-scroll-preservation-3of5

Conversation

@mj12albert

@mj12albert mj12albert commented Apr 18, 2026

Copy link
Copy Markdown
Member

Before: https://stackblitz.com/edit/oeui5u2z-2ocdxxvp?file=src%2FDemo.tsx
After: https://stackblitz.com/edit/oeui5u2z-r5vp8zge?file=src%2FDemo.tsx

Manual testing steps

1. Highlight mismatch when reopening (#48177)

  1. Click the input, then ArrowDown to move the highlight, "two" should be highlighted
  2. Press Esc to close the listbox
  3. ArrowDown once to reopen the listbox, then press Enter

Before: "two" is highlighted by step 3 and removed by Enter (the active highlight was stuck after step 1 and Esc failed to clear it)

After: "one" is highlighted by step 3 and removed by Enter


2. Adding options resets scroll position (#40250)

  1. Click the input to open the listbox
  2. Scroll once to the bottom of the listbox
  3. Wait for more options to be added

Before: when options are added the scroll position resets to the top

After: scroll position preserved when options are added

Without the scroll position fix, it's impossible to make a sane infinite loading UX; example Tanstack Query useInfiniteQuery integration with the fix: https://stackblitz.com/edit/rqmnxopa?file=src%2FDemo.tsx

Fixes #40250
Fixes #48177

@mj12albert mj12albert added type: bug It doesn't behave as expected. scope: autocomplete Changes related to the autocomplete. This includes ComboBox. labels Apr 18, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Apr 18, 2026

Copy link
Copy Markdown

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+430B(+0.08%) 🔺+94B(+0.06%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes

Deploy preview

https://deploy-preview-48322--material-ui.netlify.app/


Check out the code infra dashboard for more information about this PR.

@mj12albert mj12albert left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff is very noisy due to indentation changes from extracting out functions, so I've commented on the key changes

Comment on lines +398 to +401
if (index === -1) {
if (!preserveScroll) {
listboxNode.scrollTop = 0;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Highlight 2

Enable resets to index === -1 to opt into preserveScroll

Comment on lines 583 to 592
// Check if the previously highlighted option still exists in the updated filtered options list and if the value and inputValue haven't changed
// If it exists and the value and the inputValue haven't changed, just update its index, otherwise continue execution
const previousHighlightedOptionIndex = getPreviousHighlightedOptionIndex();
if (previousHighlightedOptionIndex !== -1) {
// Bypass setHighlightedIndex to preserve the existing highlightReasonRef.
// Keep the original highlight reason while re-syncing the DOM state.
// The highlighted option still exists after the filteredOptions array changed
// (e.g. async fetch returns new options while the user is mid-navigation),
// so the original interaction reason (keyboard, mouse, etc.) still applies.
highlightedIndexRef.current = previousHighlightedOptionIndex;
setHighlightedIndexFromSync({ index: previousHighlightedOptionIndex });
return;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Highlight 3

Instead of directly changing highlightedIndexRef.current, call the new setHighlightedIndexFromSync() instead. This is the key fix for the stale visual highlight issue

// Preserve scroll when new options are appended without changing the current filter.
const isAppendOnly =
filteredOptionsChanged &&
previousProps.inputValue === inputValue &&

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The important part of the check is previousProps.inputValue === inputValue which differentiates normal filtering from append-only loading

Comment on lines +621 to +630
if (previousProps.filteredOptions?.length > 0) {
setHighlightedIndexFromSync({ index: highlightedIndexRef.current });
return;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Highlight 4

“keep current highlighted selected option” now only applies when there were previous filtered options, so reopens don't preserve stale highlights

Comment on lines +455 to +462
const setHighlightedIndexFromSync = useEventCallback(({ index, preserveScroll = false }) => {
highlightedIndexRef.current = index;
syncHighlightedIndexToDOM({
index,
reason: highlightReasonRef.current,
preserveScroll,
});
});

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Highlight 1

setHighlightedIndex is split into

  • syncHighlightedIndexToDOM
  • setHighlightedIndex
  • setHighlightedIndexFromSync

setHighlightedIndexFromSync() is the important addition, it re-applies .Mui-focused and aria-activedescendant without changing highlightReasonRef and without firing onHighlightChange

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice splitting! I like it.

@mj12albert mj12albert force-pushed the autocomplete/fix-highlight-sync-scroll-preservation-3of5 branch from c8d6c80 to 2b37e85 Compare April 19, 2026 17:23
@mj12albert mj12albert requested a review from LukasTy April 19, 2026 19:39
@mj12albert mj12albert force-pushed the autocomplete/fix-highlight-sync-scroll-preservation-3of5 branch from 2b37e85 to 321dba2 Compare April 20, 2026 13:52
@mj12albert mj12albert force-pushed the autocomplete/fix-highlight-sync-scroll-preservation-3of5 branch from 321dba2 to a11fe24 Compare April 20, 2026 14:20
Comment thread packages/mui-material/src/useAutocomplete/useAutocomplete.js Outdated
Comment thread packages/mui-material/src/useAutocomplete/useAutocomplete.js

@ZeeshanTamboli ZeeshanTamboli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two comments related to tests structure. Rest looks good to me.

Comment thread packages/mui-material/src/Autocomplete/Autocomplete.test.js Outdated
Comment thread packages/mui-material/src/Autocomplete/Autocomplete.test.js Outdated
@mj12albert mj12albert added v7.x needs cherry-pick The PR should be cherry-picked to master after merge. labels Apr 22, 2026
@mj12albert mj12albert enabled auto-merge (squash) April 22, 2026 10:51
@mj12albert mj12albert merged commit 727c42e into mui:master Apr 22, 2026
21 checks passed
@github-actions

Copy link
Copy Markdown

Cherry-pick PRs will be created targeting branches: v7.x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs cherry-pick The PR should be cherry-picked to master after merge. scope: autocomplete Changes related to the autocomplete. This includes ComboBox. type: bug It doesn't behave as expected. v7.x

Projects

None yet

2 participants