Skip to content

[autocomplete] Fix highlight sync and scroll preservation#48322

Open
mj12albert wants to merge 1 commit intomui:masterfrom
mj12albert:autocomplete/fix-highlight-sync-scroll-preservation-3of5
Open

[autocomplete] Fix highlight sync and scroll preservation#48322
mj12albert wants to merge 1 commit intomui:masterfrom
mj12albert:autocomplete/fix-highlight-sync-scroll-preservation-3of5

Conversation

@mj12albert
Copy link
Copy Markdown
Member

@mj12albert mj12albert commented Apr 18, 2026

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
Copy link
Copy Markdown

code-infra-dashboard bot commented Apr 18, 2026

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+467B(+0.09%) 🔺+95B(+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.

Copy link
Copy Markdown
Member Author

@mj12albert mj12albert left a comment

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
// Keep the current highlighted index if possible
if (
multiple &&
currentOption &&
value.findIndex((val) => isOptionEqualToValue(currentOption, val)) !== -1
) {
return;
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

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

Labels

scope: autocomplete Changes related to the autocomplete. This includes ComboBox. type: bug It doesn't behave as expected.

Projects

None yet

1 participant